@co-engram/core 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap/classify.d.ts +67 -0
- package/dist/bootstrap/classify.d.ts.map +1 -0
- package/dist/bootstrap/classify.js +119 -0
- package/dist/bootstrap/classify.js.map +1 -0
- package/dist/bootstrap/index.d.ts +89 -0
- package/dist/bootstrap/index.d.ts.map +1 -0
- package/dist/bootstrap/index.js +158 -0
- package/dist/bootstrap/index.js.map +1 -0
- package/dist/concepts/dictionary.d.ts +260 -0
- package/dist/concepts/dictionary.d.ts.map +1 -0
- package/dist/concepts/dictionary.js +253 -0
- package/dist/concepts/dictionary.js.map +1 -0
- package/dist/concepts/index.d.ts +11 -0
- package/dist/concepts/index.d.ts.map +1 -0
- package/dist/concepts/index.js +10 -0
- package/dist/concepts/index.js.map +1 -0
- package/dist/concepts/types.d.ts +65 -0
- package/dist/concepts/types.d.ts.map +1 -0
- package/dist/concepts/types.js +15 -0
- package/dist/concepts/types.js.map +1 -0
- package/dist/config/defaults.d.ts +32 -3
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +52 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/index.d.ts +18 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +75 -3
- package/dist/config/index.js.map +1 -1
- package/dist/config/loader.d.ts +10 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +10 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +97 -1
- package/dist/config/types.d.ts.map +1 -1
- package/dist/contradiction/auto-degrade.d.ts +11 -2
- package/dist/contradiction/auto-degrade.d.ts.map +1 -1
- package/dist/contradiction/auto-degrade.js +22 -0
- package/dist/contradiction/auto-degrade.js.map +1 -1
- package/dist/contradiction/resolver.d.ts.map +1 -1
- package/dist/contradiction/resolver.js +7 -1
- package/dist/contradiction/resolver.js.map +1 -1
- package/dist/dreaming/index.d.ts +1 -0
- package/dist/dreaming/index.d.ts.map +1 -1
- package/dist/dreaming/index.js +1 -0
- package/dist/dreaming/index.js.map +1 -1
- package/dist/dreaming/llm-pattern-abstraction.d.ts +31 -0
- package/dist/dreaming/llm-pattern-abstraction.d.ts.map +1 -0
- package/dist/dreaming/llm-pattern-abstraction.js +70 -0
- package/dist/dreaming/llm-pattern-abstraction.js.map +1 -0
- package/dist/dreaming/rem.d.ts.map +1 -1
- package/dist/dreaming/rem.js +1 -0
- package/dist/dreaming/rem.js.map +1 -1
- package/dist/dreaming/scheduler.d.ts +13 -0
- package/dist/dreaming/scheduler.d.ts.map +1 -1
- package/dist/dreaming/scheduler.js +14 -2
- package/dist/dreaming/scheduler.js.map +1 -1
- package/dist/evolution/triggered.d.ts.map +1 -1
- package/dist/evolution/triggered.js +1 -0
- package/dist/evolution/triggered.js.map +1 -1
- package/dist/generative/hypothesis.d.ts.map +1 -1
- package/dist/generative/hypothesis.js +1 -0
- package/dist/generative/hypothesis.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1262 -32
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/index.d.ts +34 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/index.js +36 -7
- package/dist/i18n/index.js.map +1 -1
- package/dist/i18n/zh.d.ts +698 -32
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +1267 -33
- package/dist/i18n/zh.js.map +1 -1
- package/dist/index/graph-builder.js +3 -3
- package/dist/index/graph-builder.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/learning/loop.d.ts +9 -0
- package/dist/learning/loop.d.ts.map +1 -1
- package/dist/learning/loop.js +42 -1
- package/dist/learning/loop.js.map +1 -1
- package/dist/maintenance/types.d.ts +11 -0
- package/dist/maintenance/types.d.ts.map +1 -1
- package/dist/maintenance/types.js.map +1 -1
- package/dist/merge/synapse-merger.js +6 -0
- package/dist/merge/synapse-merger.js.map +1 -1
- package/dist/merge-driver.cjs +64 -5
- package/dist/observability/audit-log.d.ts +7 -1
- package/dist/observability/audit-log.d.ts.map +1 -1
- package/dist/observability/audit-log.js.map +1 -1
- package/dist/observability/necessity-evaluator.d.ts +29 -0
- package/dist/observability/necessity-evaluator.d.ts.map +1 -1
- package/dist/observability/necessity-evaluator.js +240 -13
- package/dist/observability/necessity-evaluator.js.map +1 -1
- package/dist/observability/proposal-engine.d.ts.map +1 -1
- package/dist/observability/proposal-engine.js +12 -0
- package/dist/observability/proposal-engine.js.map +1 -1
- package/dist/observability/runtime-description-check.d.ts +55 -0
- package/dist/observability/runtime-description-check.d.ts.map +1 -0
- package/dist/observability/runtime-description-check.js +63 -0
- package/dist/observability/runtime-description-check.js.map +1 -0
- package/dist/prompt-signals/cache.d.ts +73 -0
- package/dist/prompt-signals/cache.d.ts.map +1 -1
- package/dist/prompt-signals/cache.js +102 -0
- package/dist/prompt-signals/cache.js.map +1 -1
- package/dist/prompt-signals/event-bus.d.ts +82 -0
- package/dist/prompt-signals/event-bus.d.ts.map +1 -0
- package/dist/prompt-signals/event-bus.js +105 -0
- package/dist/prompt-signals/event-bus.js.map +1 -0
- package/dist/prompt-signals/index.d.ts +2 -1
- package/dist/prompt-signals/index.d.ts.map +1 -1
- package/dist/prompt-signals/index.js +2 -1
- package/dist/prompt-signals/index.js.map +1 -1
- package/dist/reinforcement/ltp.d.ts +15 -1
- package/dist/reinforcement/ltp.d.ts.map +1 -1
- package/dist/reinforcement/ltp.js +24 -5
- package/dist/reinforcement/ltp.js.map +1 -1
- package/dist/reinforcement/related.d.ts +31 -2
- package/dist/reinforcement/related.d.ts.map +1 -1
- package/dist/reinforcement/related.js +39 -3
- package/dist/reinforcement/related.js.map +1 -1
- package/dist/retrieval/filter.d.ts.map +1 -1
- package/dist/retrieval/filter.js +7 -0
- package/dist/retrieval/filter.js.map +1 -1
- package/dist/retrieval/fts.d.ts +6 -5
- package/dist/retrieval/fts.d.ts.map +1 -1
- package/dist/retrieval/fts.js +74 -22
- package/dist/retrieval/fts.js.map +1 -1
- package/dist/status/index.d.ts +7 -0
- package/dist/status/index.d.ts.map +1 -0
- package/dist/status/index.js +7 -0
- package/dist/status/index.js.map +1 -0
- package/dist/status/status.d.ts +132 -0
- package/dist/status/status.d.ts.map +1 -0
- package/dist/status/status.js +437 -0
- package/dist/status/status.js.map +1 -0
- package/dist/storage/engram-store.d.ts.map +1 -1
- package/dist/storage/engram-store.js +17 -2
- package/dist/storage/engram-store.js.map +1 -1
- package/dist/storage/git.d.ts +168 -0
- package/dist/storage/git.d.ts.map +1 -1
- package/dist/storage/git.js +616 -33
- package/dist/storage/git.js.map +1 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +1 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/infra-doctor.d.ts +42 -0
- package/dist/storage/infra-doctor.d.ts.map +1 -0
- package/dist/storage/infra-doctor.js +92 -0
- package/dist/storage/infra-doctor.js.map +1 -0
- package/dist/storage/obsidian-links.d.ts +73 -0
- package/dist/storage/obsidian-links.d.ts.map +1 -0
- package/dist/storage/obsidian-links.js +177 -0
- package/dist/storage/obsidian-links.js.map +1 -0
- package/dist/storage/path.d.ts +24 -0
- package/dist/storage/path.d.ts.map +1 -1
- package/dist/storage/path.js +53 -0
- package/dist/storage/path.js.map +1 -1
- package/dist/storage/repository.d.ts +37 -1
- package/dist/storage/repository.d.ts.map +1 -1
- package/dist/storage/repository.js +235 -17
- package/dist/storage/repository.js.map +1 -1
- package/dist/storage/synapse-store.d.ts +7 -1
- package/dist/storage/synapse-store.d.ts.map +1 -1
- package/dist/storage/synapse-store.js +8 -0
- package/dist/storage/synapse-store.js.map +1 -1
- package/dist/tools/audit-query-tool.d.ts +53 -0
- package/dist/tools/audit-query-tool.d.ts.map +1 -0
- package/dist/tools/audit-query-tool.js +123 -0
- package/dist/tools/audit-query-tool.js.map +1 -0
- package/dist/tools/doctor-tools.d.ts +5 -0
- package/dist/tools/doctor-tools.d.ts.map +1 -1
- package/dist/tools/doctor-tools.js +11 -3
- package/dist/tools/doctor-tools.js.map +1 -1
- package/dist/tools/engram-tools.d.ts +13 -0
- package/dist/tools/engram-tools.d.ts.map +1 -1
- package/dist/tools/engram-tools.js +72 -8
- package/dist/tools/engram-tools.js.map +1 -1
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/llm-descriptions.d.ts +28 -28
- package/dist/tools/llm-descriptions.d.ts.map +1 -1
- package/dist/tools/llm-descriptions.js +56 -489
- package/dist/tools/llm-descriptions.js.map +1 -1
- package/dist/tools/normalization.d.ts +43 -0
- package/dist/tools/normalization.d.ts.map +1 -0
- package/dist/tools/normalization.js +68 -0
- package/dist/tools/normalization.js.map +1 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +6 -0
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/schemas.d.ts +67 -11
- package/dist/tools/schemas.d.ts.map +1 -1
- package/dist/tools/schemas.js +61 -6
- package/dist/tools/schemas.js.map +1 -1
- package/dist/tools/skill-tools.js +1 -1
- package/dist/tools/skill-tools.js.map +1 -1
- package/dist/tools/synapse-tools.d.ts.map +1 -1
- package/dist/tools/synapse-tools.js +1 -0
- package/dist/tools/synapse-tools.js.map +1 -1
- package/dist/tools/sync-tools.d.ts +102 -0
- package/dist/tools/sync-tools.d.ts.map +1 -0
- package/dist/tools/sync-tools.js +309 -0
- package/dist/tools/sync-tools.js.map +1 -0
- package/dist/tools/synthesize-tools.d.ts +79 -0
- package/dist/tools/synthesize-tools.d.ts.map +1 -0
- package/dist/tools/synthesize-tools.js +297 -0
- package/dist/tools/synthesize-tools.js.map +1 -0
- package/dist/tools/tool-profile.d.ts +68 -0
- package/dist/tools/tool-profile.d.ts.map +1 -0
- package/dist/tools/tool-profile.js +174 -0
- package/dist/tools/tool-profile.js.map +1 -0
- package/dist/tools/tool.d.ts +17 -0
- package/dist/tools/tool.d.ts.map +1 -1
- package/dist/tools/tool.js.map +1 -1
- package/dist/types/disclosure.d.ts +7 -0
- package/dist/types/disclosure.d.ts.map +1 -1
- package/dist/types/repository-types.d.ts +17 -1
- package/dist/types/repository-types.d.ts.map +1 -1
- package/dist/types/synapse.d.ts +19 -1
- package/dist/types/synapse.d.ts.map +1 -1
- package/package.json +9 -9
package/dist/storage/git.js
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
* 不依赖外部 npm 包,保持 host-agnostic。
|
|
5
5
|
* 只封装 co-engram 需要的最小操作集。
|
|
6
6
|
*
|
|
7
|
+
* 公司内外部兼容性策略:
|
|
8
|
+
* - 直接调用系统 `git`,继承用户 SSH/credentials/proxy 环境
|
|
9
|
+
* - 不硬编码任何主机名/URL/refspec(Gerrit review 由用户 .git/config 决定)
|
|
10
|
+
* - 不主动写 Change-Id(ZTE/Gerrit commit-msg hook 若已装会自动加)
|
|
11
|
+
*
|
|
7
12
|
* @module @co-engram/core/storage
|
|
8
13
|
*/
|
|
9
|
-
import { execSync } from "node:child_process";
|
|
10
|
-
import { existsSync } from "node:fs";
|
|
14
|
+
import { execSync, spawnSync } from "node:child_process";
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
16
|
import { join } from "node:path";
|
|
12
17
|
/**
|
|
13
18
|
* 检查路径是否是 Git 仓库
|
|
@@ -50,59 +55,98 @@ export function initGitRepo(repoPath) {
|
|
|
50
55
|
* Stage 文件并提交
|
|
51
56
|
*
|
|
52
57
|
* 注意:不会自动 push,团队需要时手动 push。
|
|
58
|
+
*
|
|
59
|
+
* 实现安全说明(Task 4.2):用 `spawnSync` + 数组参数(`args[]`)而非
|
|
60
|
+
* `execSync` + 字符串拼接,避免 shell 元字符(backtick / $ / 反引号)
|
|
61
|
+
* 在 authorName / message / file 路径里触发命令注入。数组参数下 git
|
|
62
|
+
* 直接收到原始字符串,不经 shell 解释。
|
|
53
63
|
*/
|
|
54
64
|
export function commitFiles(options) {
|
|
55
65
|
const { repoPath, files, message, authorName, authorEmail } = options;
|
|
56
66
|
if (!isGitRepo(repoPath)) {
|
|
57
67
|
initGitRepo(repoPath);
|
|
58
68
|
}
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
execSync(`git add ${fileArgs}`, { cwd: repoPath, stdio: "ignore" });
|
|
63
|
-
}
|
|
64
|
-
else {
|
|
65
|
-
execSync("git add -A", { cwd: repoPath, stdio: "ignore" });
|
|
66
|
-
}
|
|
67
|
-
// Check if there are staged changes
|
|
68
|
-
let hasStagedChanges = true;
|
|
69
|
+
// P0-7:中文路径默认被 git 用 <U+XXXX> 转义,changedFiles 显示为
|
|
70
|
+
// "\xxx\xxx\xxx" 不可读。在 commit 入口设 core.quotepath=false
|
|
71
|
+
// (local repo,不污染 global);失败不阻断 commit —— 这只是显示优化。
|
|
69
72
|
try {
|
|
70
|
-
|
|
71
|
-
hasStagedChanges = false; // exit 0 means no changes
|
|
73
|
+
runGitSpawn(repoPath, ["config", "core.quotepath", "false"]);
|
|
72
74
|
}
|
|
73
75
|
catch {
|
|
74
|
-
|
|
76
|
+
// config 设置失败(如 .git/config 权限问题)不阻断 commit
|
|
77
|
+
}
|
|
78
|
+
// Stage files —— 数组参数,文件名含空格 / 特殊字符都无需 shell 转义
|
|
79
|
+
if (files.length > 0) {
|
|
80
|
+
runGitSpawn(repoPath, ["add", ...files]);
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
else {
|
|
83
|
+
runGitSpawn(repoPath, ["add", "-A"]);
|
|
84
|
+
}
|
|
85
|
+
// Check if there are staged changes(git diff --cached --quiet 退出码:
|
|
86
|
+
// 0 = 无变化,1 = 有变化)
|
|
87
|
+
const diffResult = spawnSync("git", ["diff", "--cached", "--quiet"], { cwd: repoPath, stdio: "ignore" });
|
|
88
|
+
if (diffResult.status === 0) {
|
|
77
89
|
// Nothing to commit
|
|
78
90
|
const branch = getCurrentBranch(repoPath);
|
|
79
91
|
return { commitHash: "", branch, filesChanged: 0 };
|
|
80
92
|
}
|
|
81
|
-
// Commit
|
|
82
|
-
|
|
93
|
+
// Commit —— 用 -c user.name=... -c user.email=... 临时覆盖;数组参数
|
|
94
|
+
// 让 authorName 含 backtick / $ 等也字面保留,不触发 shell
|
|
95
|
+
const commitArgs = [];
|
|
83
96
|
if (authorName) {
|
|
84
|
-
|
|
97
|
+
commitArgs.push("-c", `user.name=${authorName}`);
|
|
85
98
|
}
|
|
86
99
|
if (authorEmail) {
|
|
87
|
-
|
|
100
|
+
commitArgs.push("-c", `user.email=${authorEmail}`);
|
|
88
101
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
102
|
+
commitArgs.push("commit", "-m", message);
|
|
103
|
+
runGitSpawn(repoPath, commitArgs);
|
|
104
|
+
const commitHash = spawnSyncOutput(repoPath, ["rev-parse", "HEAD"]).trim();
|
|
105
|
+
const branch = getCurrentBranch(repoPath);
|
|
106
|
+
// Count changed files in this commit(spawn 版,无 shell pipeline)
|
|
107
|
+
// 用 `git show --name-only` 对首次 commit(无 HEAD~1)也工作
|
|
108
|
+
const showNames = spawnSyncOutput(repoPath, [
|
|
109
|
+
"show",
|
|
110
|
+
"--name-only",
|
|
111
|
+
"--pretty=format:",
|
|
112
|
+
"HEAD",
|
|
113
|
+
]).trim();
|
|
114
|
+
const filesChanged = showNames
|
|
115
|
+
.split("\n")
|
|
116
|
+
.map((l) => l.trim())
|
|
117
|
+
.filter(Boolean).length;
|
|
118
|
+
return { commitHash, branch, filesChanged };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 内部 helper:用 spawnSync 跑 git 命令,失败抛错。
|
|
122
|
+
*
|
|
123
|
+
* 数组参数绕过 shell,任何字符(包括 backtick / $ / 引号 / 换行)都按
|
|
124
|
+
* 字面传递给 git。是 Task 4.2 防 shell 注入的核心。
|
|
125
|
+
*/
|
|
126
|
+
function runGitSpawn(repoPath, args) {
|
|
127
|
+
const result = spawnSync("git", args, {
|
|
96
128
|
cwd: repoPath,
|
|
97
129
|
encoding: "utf8",
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
131
|
+
});
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
const stderr = (result.stderr ?? "").trim();
|
|
134
|
+
throw new Error(`git ${args.join(" ")} failed (status=${result.status}): ${stderr}`);
|
|
135
|
+
}
|
|
136
|
+
return result.stdout ?? "";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 内部 helper:用 spawnSync 跑 git 命令取 stdout,失败返回空串。
|
|
140
|
+
*/
|
|
141
|
+
function spawnSyncOutput(repoPath, args) {
|
|
142
|
+
const result = spawnSync("git", args, {
|
|
102
143
|
cwd: repoPath,
|
|
103
144
|
encoding: "utf8",
|
|
104
|
-
|
|
105
|
-
|
|
145
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
146
|
+
});
|
|
147
|
+
if (result.status !== 0)
|
|
148
|
+
return "";
|
|
149
|
+
return result.stdout ?? "";
|
|
106
150
|
}
|
|
107
151
|
/**
|
|
108
152
|
* 获取当前分支名
|
|
@@ -153,4 +197,543 @@ export function resolveRepoPath(repoPath) {
|
|
|
153
197
|
}
|
|
154
198
|
return repoPath;
|
|
155
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* `git pull --rebase` —— 用 rebase 策略保持线性历史。
|
|
202
|
+
*
|
|
203
|
+
* 冲突时执行 `git rebase --abort` 回到 pull 前状态,并返回 conflicts 清单,
|
|
204
|
+
* 让调用方决定下一步(人工解决 / 放弃)。绝不自动 resolve。
|
|
205
|
+
*
|
|
206
|
+
* 远端无新提交时返回 `{ ok: true, upToDate: true }`。
|
|
207
|
+
*
|
|
208
|
+
* 注意:调用方需保证 repoPath 是 git 仓库(用 isGitRepo 预检)。
|
|
209
|
+
*/
|
|
210
|
+
export function pullRepo(repoPath) {
|
|
211
|
+
// 先 fetch,再尝试 rebase。分离两步便于精准识别"无远端更新"vs"rebase 冲突"。
|
|
212
|
+
// --quiet 抑制 fetch 的进度噪声;--prune 让 deleted remote branch 同步。
|
|
213
|
+
try {
|
|
214
|
+
execSync("git fetch --quiet --prune", {
|
|
215
|
+
cwd: repoPath,
|
|
216
|
+
stdio: "pipe",
|
|
217
|
+
encoding: "utf8",
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// fetch 失败:无远端 / 网络问题。降级为 "upToDate"(让 commit 阶段继续)。
|
|
222
|
+
// 真正的"无远端"由 hasRemote 预检过滤;这里只剩网络问题。
|
|
223
|
+
return { ok: true, upToDate: true };
|
|
224
|
+
}
|
|
225
|
+
// 比较本地与上游:HEAD...@{upstream}
|
|
226
|
+
// 若无 upstream,直接 upToDate(本地分支未跟踪远端,pull 无意义)
|
|
227
|
+
let upstreamAvailable = true;
|
|
228
|
+
try {
|
|
229
|
+
execSync("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}", {
|
|
230
|
+
cwd: repoPath,
|
|
231
|
+
stdio: "pipe",
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
upstreamAvailable = false;
|
|
237
|
+
}
|
|
238
|
+
if (!upstreamAvailable) {
|
|
239
|
+
return { ok: true, upToDate: true };
|
|
240
|
+
}
|
|
241
|
+
// 计算远端领先的提交数
|
|
242
|
+
let behindCount = 0;
|
|
243
|
+
try {
|
|
244
|
+
const raw = execSync("git rev-list --count HEAD..@{upstream}", { cwd: repoPath, stdio: "pipe", encoding: "utf8" }).trim();
|
|
245
|
+
behindCount = parseInt(raw, 10) || 0;
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
behindCount = 0;
|
|
249
|
+
}
|
|
250
|
+
if (behindCount === 0) {
|
|
251
|
+
return { ok: true, upToDate: true };
|
|
252
|
+
}
|
|
253
|
+
// 真正 rebase:--autostash 让本地未提交变更自动暂存
|
|
254
|
+
try {
|
|
255
|
+
execSync("git pull --rebase --autostash", {
|
|
256
|
+
cwd: repoPath,
|
|
257
|
+
stdio: "pipe",
|
|
258
|
+
encoding: "utf8",
|
|
259
|
+
});
|
|
260
|
+
return { ok: true, fetchedCount: behindCount };
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
// rebase 冲突:提取冲突文件清单,然后 abort 回到 pull 前状态
|
|
264
|
+
const conflicts = listRebaseConflicts(repoPath);
|
|
265
|
+
try {
|
|
266
|
+
execSync("git rebase --abort", {
|
|
267
|
+
cwd: repoPath,
|
|
268
|
+
stdio: "ignore",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
// abort 失败说明不在 rebase 状态(可能冲突已自动跳出),忽略
|
|
273
|
+
}
|
|
274
|
+
return { ok: false, conflicts };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* 列出当前 rebase 冲突中的文件(相对仓库根路径)。
|
|
279
|
+
*
|
|
280
|
+
* 用 `git diff --name-only --diff-filter=U`:列出 unmerged paths。
|
|
281
|
+
* 仅在 rebase/merge 进行中或索引有冲突标记时返回非空。
|
|
282
|
+
*/
|
|
283
|
+
function listRebaseConflicts(repoPath) {
|
|
284
|
+
try {
|
|
285
|
+
const raw = execSync("git diff --name-only --diff-filter=U", {
|
|
286
|
+
cwd: repoPath,
|
|
287
|
+
stdio: "pipe",
|
|
288
|
+
encoding: "utf8",
|
|
289
|
+
});
|
|
290
|
+
return raw.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* 检测 stderr 是否是 Gerrit 对受保护分支的拒绝特征。
|
|
298
|
+
*
|
|
299
|
+
* Gerrit 对 master 等受保护分支的直接 push 返回 "prohibited by Gerrit"
|
|
300
|
+
* 或 "need 'Push' rights"。命中后可 fallback 到 `refs/for/<branch>` 走 review。
|
|
301
|
+
*/
|
|
302
|
+
export function isGerritRejection(stderr) {
|
|
303
|
+
return /prohibited by Gerrit|need ['"]Push['"] rights/i.test(stderr);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* 用 `refs/for/<branch>` refspec 推送(Gerrit code-review 流程)。
|
|
307
|
+
*
|
|
308
|
+
* spawnSync + 数组参数,与 commitFiles 安全模型一致:branch 名含特殊字符
|
|
309
|
+
* 也按字面传递给 git,不经 shell 解释。
|
|
310
|
+
*/
|
|
311
|
+
function tryGerritReviewPush(repoPath, remote, branch) {
|
|
312
|
+
const result = spawnSync("git", ["push", remote, `HEAD:refs/for/${branch}`], {
|
|
313
|
+
cwd: repoPath,
|
|
314
|
+
encoding: "utf8",
|
|
315
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
316
|
+
});
|
|
317
|
+
if (result.status === 0) {
|
|
318
|
+
return { ok: true };
|
|
319
|
+
}
|
|
320
|
+
const stderr = (result.stderr ?? "").trim();
|
|
321
|
+
return {
|
|
322
|
+
ok: false,
|
|
323
|
+
reason: `gerrit-review push to refs/for/${branch} failed: ${stderr || `exit status ${result.status ?? "unknown"}`}`,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* push 失败的统一处理:若 stderr 命中 Gerrit 拒绝特征,fallback 到 review refspec。
|
|
328
|
+
*
|
|
329
|
+
* execSync 失败时 Error.stderr 含 git 原始 stderr(encoding=utf8 时为 string),
|
|
330
|
+
* 兜底用 err.message。返回带 mode/autoFallback 标记的 GitPushResult。
|
|
331
|
+
*/
|
|
332
|
+
function handlePushFailure(repoPath, remote, err, directLabel) {
|
|
333
|
+
const errStderr = err.stderr;
|
|
334
|
+
const stderr = typeof errStderr === "string"
|
|
335
|
+
? errStderr
|
|
336
|
+
: Buffer.isBuffer(errStderr)
|
|
337
|
+
? errStderr.toString("utf8")
|
|
338
|
+
: "";
|
|
339
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
340
|
+
const combined = stderr.trim() || msg;
|
|
341
|
+
if (isGerritRejection(combined)) {
|
|
342
|
+
const branch = getCurrentBranch(repoPath);
|
|
343
|
+
const review = tryGerritReviewPush(repoPath, remote, branch);
|
|
344
|
+
if (review.ok) {
|
|
345
|
+
return {
|
|
346
|
+
ok: true,
|
|
347
|
+
skipped: false,
|
|
348
|
+
remote,
|
|
349
|
+
mode: "gerrit-review",
|
|
350
|
+
autoFallback: true,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
ok: false,
|
|
355
|
+
skipped: false,
|
|
356
|
+
remote,
|
|
357
|
+
mode: "gerrit-review",
|
|
358
|
+
reason: review.reason,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
ok: false,
|
|
363
|
+
skipped: false,
|
|
364
|
+
remote,
|
|
365
|
+
mode: "direct",
|
|
366
|
+
reason: `${directLabel}: ${combined}`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* `git push` —— 推送当前分支,失败时若检测到 Gerrit 拒绝则自动 fallback 到 review。
|
|
371
|
+
*
|
|
372
|
+
* 行为:
|
|
373
|
+
* - 有 upstream → `git push`(尊重 push.default 配置)
|
|
374
|
+
* - 无 upstream → `git push --set-upstream <remote> <branch>`
|
|
375
|
+
* - 任一路径失败且 stderr 命中 Gerrit 拒绝特征 → 自动重试
|
|
376
|
+
* `git push <remote> HEAD:refs/for/<branch>`(spawnSync 数组参数,防注入)
|
|
377
|
+
*
|
|
378
|
+
* 兼容 GitHub / GitLab / Gerrit / 内部 Git 服务,不硬编码 URL 或 refspec。
|
|
379
|
+
* Gerrit 用户无需手动配置 .git/config 的 `push = refs/heads/*:refs/for/*`,
|
|
380
|
+
* 检测到 direct push 被拒即自动走 review(返回 mode="gerrit-review", autoFallback=true)。
|
|
381
|
+
*
|
|
382
|
+
* 不抛错 —— 返回结构化结果让工具层向用户报告。
|
|
383
|
+
*/
|
|
384
|
+
export function pushRepo(repoPath) {
|
|
385
|
+
// 检测 remote
|
|
386
|
+
let remote = "";
|
|
387
|
+
try {
|
|
388
|
+
remote = execSync("git remote", {
|
|
389
|
+
cwd: repoPath,
|
|
390
|
+
stdio: "pipe",
|
|
391
|
+
encoding: "utf8",
|
|
392
|
+
})
|
|
393
|
+
.split("\n")
|
|
394
|
+
.map((l) => l.trim())
|
|
395
|
+
.filter(Boolean)[0] ?? "";
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return {
|
|
399
|
+
ok: false,
|
|
400
|
+
skipped: true,
|
|
401
|
+
reason: "no remote configured (commit only, no push)",
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
if (!remote) {
|
|
405
|
+
return {
|
|
406
|
+
ok: false,
|
|
407
|
+
skipped: true,
|
|
408
|
+
reason: "no remote configured (commit only, no push)",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// 检测当前分支是否有 upstream
|
|
412
|
+
try {
|
|
413
|
+
execSync("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}", {
|
|
414
|
+
cwd: repoPath,
|
|
415
|
+
stdio: "pipe",
|
|
416
|
+
encoding: "utf8",
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
// 无 upstream:尝试用当前分支名推到 remote,设上游
|
|
421
|
+
const branch = getCurrentBranch(repoPath);
|
|
422
|
+
try {
|
|
423
|
+
execSync(`git push --set-upstream ${remote} ${branch}`, {
|
|
424
|
+
cwd: repoPath,
|
|
425
|
+
stdio: "pipe",
|
|
426
|
+
encoding: "utf8",
|
|
427
|
+
});
|
|
428
|
+
return { ok: true, skipped: false, remote, mode: "direct" };
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
return handlePushFailure(repoPath, remote, err, `push --set-upstream ${remote} ${branch} failed`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
// 正常 push(已有 upstream)
|
|
435
|
+
try {
|
|
436
|
+
execSync("git push", {
|
|
437
|
+
cwd: repoPath,
|
|
438
|
+
stdio: "pipe",
|
|
439
|
+
encoding: "utf8",
|
|
440
|
+
});
|
|
441
|
+
return { ok: true, skipped: false, remote, mode: "direct" };
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
return handlePushFailure(repoPath, remote, err, "push rejected");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 获取 `git status --short` 解析后的变更清单。
|
|
449
|
+
*
|
|
450
|
+
* 用于 dryRun 预览 + 提交后校验 filesChanged。
|
|
451
|
+
* 返回相对仓库根的路径数组(去重 + 排序,确保跨平台一致)。
|
|
452
|
+
*/
|
|
453
|
+
export function getGitStatusShort(repoPath) {
|
|
454
|
+
try {
|
|
455
|
+
const raw = execSync("git status --short --untracked-files=all", {
|
|
456
|
+
cwd: repoPath,
|
|
457
|
+
stdio: "pipe",
|
|
458
|
+
encoding: "utf8",
|
|
459
|
+
});
|
|
460
|
+
return raw
|
|
461
|
+
.split("\n")
|
|
462
|
+
.map((line) => {
|
|
463
|
+
// 格式:"XY path" 或 "XY orig -> renamed"
|
|
464
|
+
const trimmed = line.trimEnd();
|
|
465
|
+
if (!trimmed)
|
|
466
|
+
return "";
|
|
467
|
+
const pathPart = trimmed.slice(3);
|
|
468
|
+
if (pathPart.includes(" -> ")) {
|
|
469
|
+
return pathPart.split(" -> ").at(-1) ?? pathPart;
|
|
470
|
+
}
|
|
471
|
+
return pathPart;
|
|
472
|
+
})
|
|
473
|
+
.filter(Boolean);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 是否配置了 git remote。
|
|
481
|
+
*
|
|
482
|
+
* 用于工具层在 push=true 时预判"无 remote → commit-only 降级"路径。
|
|
483
|
+
*/
|
|
484
|
+
export function hasRemote(repoPath) {
|
|
485
|
+
try {
|
|
486
|
+
const raw = execSync("git remote", {
|
|
487
|
+
cwd: repoPath,
|
|
488
|
+
stdio: "pipe",
|
|
489
|
+
encoding: "utf8",
|
|
490
|
+
});
|
|
491
|
+
return raw
|
|
492
|
+
.split("\n")
|
|
493
|
+
.map((l) => l.trim())
|
|
494
|
+
.some(Boolean);
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* 默认 .gitignore 模板(co-engram 仓库)。
|
|
502
|
+
*
|
|
503
|
+
* 设计原则:整个 `.co-engram/` 目录不入库(派生数据 + 行为缓存 +
|
|
504
|
+
* 审计日志,均可重新生成或本地保留),只跟踪用户内容(*.md, synapses/)。
|
|
505
|
+
*
|
|
506
|
+
* Obsidian 用户态文件(workspace.json 等)每台机器不同,不入库;
|
|
507
|
+
* 团队级配置(app.json, appearance.json 等)可入库,由用户决定。
|
|
508
|
+
*/
|
|
509
|
+
export const DEFAULT_CO_ENGRAM_GITIGNORE = `# co-engram 仓库默认 .gitignore
|
|
510
|
+
# 整个 .co-engram/ 目录不入库(派生数据 + 行为缓存 + 审计日志,可重新生成)
|
|
511
|
+
.co-engram/
|
|
512
|
+
|
|
513
|
+
# Private engrams(visibility='private')—— 用户私人记忆,不入团队仓库
|
|
514
|
+
# 本机 agent 仍可索引/检索;只在 git sync 层隔离。
|
|
515
|
+
# 用户若想保留 private engram 的 git 历史,可在 private/ 子目录建独立 git 仓库。
|
|
516
|
+
private/
|
|
517
|
+
|
|
518
|
+
# Obsidian 用户态(每台机器不同)
|
|
519
|
+
.obsidian/workspace.json
|
|
520
|
+
.obsidian/workspace-mobile.json
|
|
521
|
+
.obsidian/workspace.json.bak
|
|
522
|
+
.obsidian/cache
|
|
523
|
+
.obsidian/plugins/*/data.json
|
|
524
|
+
|
|
525
|
+
# 系统
|
|
526
|
+
.DS_Store
|
|
527
|
+
Thumbs.db
|
|
528
|
+
*.swp
|
|
529
|
+
*.swo
|
|
530
|
+
`;
|
|
531
|
+
/**
|
|
532
|
+
* 若缺失则创建 .gitignore(用 DEFAULT_CO_ENGRAM_GITIGNORE 模板)。
|
|
533
|
+
*
|
|
534
|
+
* @returns true 表示本次创建,false 表示已存在未动。
|
|
535
|
+
*/
|
|
536
|
+
export function ensureGitignore(repoPath) {
|
|
537
|
+
const gitignorePath = join(repoPath, ".gitignore");
|
|
538
|
+
if (existsSync(gitignorePath)) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
writeFileSync(gitignorePath, DEFAULT_CO_ENGRAM_GITIGNORE, "utf8");
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* 向已存在的 .gitignore 追加规则(幂等)。
|
|
546
|
+
*
|
|
547
|
+
* 用于让历史已存在的 .gitignore 升级,补上新规则(如 `private/`)。
|
|
548
|
+
* 与 `ensureGitignore` 互补:后者只处理"文件不存在"场景。
|
|
549
|
+
*
|
|
550
|
+
* 幂等性:若 .gitignore 已含相同 rule 行(忽略首尾空白),不重复追加。
|
|
551
|
+
*
|
|
552
|
+
* @returns true 表示本次追加,false 表示已含该 rule 或 .gitignore 不存在。
|
|
553
|
+
*/
|
|
554
|
+
export function appendToGitignore(repoPath, rule) {
|
|
555
|
+
const gitignorePath = join(repoPath, ".gitignore");
|
|
556
|
+
if (!existsSync(gitignorePath)) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
const content = readFileSync(gitignorePath, "utf8");
|
|
560
|
+
// 逐行比对,trim 后相等视为已含(避免末尾空白差异导致重复)
|
|
561
|
+
const lines = content.split(/\r?\n/);
|
|
562
|
+
for (const line of lines) {
|
|
563
|
+
if (line.trim() === rule.trim()) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// 追加:确保前一个空行存在,避免粘连上一行
|
|
568
|
+
const prefix = content.endsWith("\n") ? "" : "\n";
|
|
569
|
+
const separator = content.trim().length === 0 ? "" : "\n";
|
|
570
|
+
writeFileSync(gitignorePath, `${content}${prefix}${separator}${rule}\n`, "utf8");
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 把 `.co-engram/` 目录从 git index 移除(磁盘文件保留)。
|
|
575
|
+
*
|
|
576
|
+
* `.gitignore` 只对未 track 的文件生效;若历史 commit 已 track 了
|
|
577
|
+
* `.co-engram/*`,新建 .gitignore 后这些文件仍会被 commit。
|
|
578
|
+
*
|
|
579
|
+
* 本函数检测这一情况,若已 track 则 `git rm -r --cached --quiet .co-engram/`,
|
|
580
|
+
* 让缓存目录真正退出 git 跟踪(磁盘文件原样保留,不影响运行时)。
|
|
581
|
+
*
|
|
582
|
+
* **重要副作用:** commit 描述是"删除这些文件"。协作者 pull 此 commit 时,
|
|
583
|
+
* 他们本地的 `.co-engram/*` 磁盘文件也会被 git 删除(因为 commit 说删)。
|
|
584
|
+
* 若协作者本地有不可再生的数据(如 audit.jsonl 团队审计),会丢失。
|
|
585
|
+
* 调用方应让用户显式 opt-in,而非默认执行。
|
|
586
|
+
*
|
|
587
|
+
* @returns 移除的文件数(0 = 本来就没 track 或失败)。
|
|
588
|
+
*/
|
|
589
|
+
export function untrackCoEngramCache(repoPath) {
|
|
590
|
+
// 先检测是否有 tracked 文件
|
|
591
|
+
let trackedRaw = "";
|
|
592
|
+
try {
|
|
593
|
+
trackedRaw = execSync("git ls-files .co-engram/", {
|
|
594
|
+
cwd: repoPath,
|
|
595
|
+
stdio: "pipe",
|
|
596
|
+
encoding: "utf8",
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
return 0;
|
|
601
|
+
}
|
|
602
|
+
const tracked = trackedRaw
|
|
603
|
+
.split("\n")
|
|
604
|
+
.map((l) => l.trim())
|
|
605
|
+
.filter(Boolean);
|
|
606
|
+
if (tracked.length === 0) {
|
|
607
|
+
return 0;
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
execSync("git rm -r --cached --quiet .co-engram/", {
|
|
611
|
+
cwd: repoPath,
|
|
612
|
+
stdio: "pipe",
|
|
613
|
+
encoding: "utf8",
|
|
614
|
+
});
|
|
615
|
+
return tracked.length;
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 只读检测:`.co-engram/` 下已 tracked 的文件数(不修改 index)。
|
|
623
|
+
*
|
|
624
|
+
* 用于 dryRun 预测 + 工具层判断是否需要提示用户 opt-in untrack。
|
|
625
|
+
*/
|
|
626
|
+
export function countTrackedCoEngramCache(repoPath) {
|
|
627
|
+
try {
|
|
628
|
+
const raw = execSync("git ls-files .co-engram/", {
|
|
629
|
+
cwd: repoPath,
|
|
630
|
+
stdio: "pipe",
|
|
631
|
+
encoding: "utf8",
|
|
632
|
+
});
|
|
633
|
+
return raw
|
|
634
|
+
.split("\n")
|
|
635
|
+
.map((l) => l.trim())
|
|
636
|
+
.filter(Boolean).length;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return 0;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* 把 `private/` 目录从 git index 移除(磁盘文件保留)。
|
|
644
|
+
*
|
|
645
|
+
* `private/` 已加入 DEFAULT_CO_ENGRAM_GITIGNORE,新文件自动跳过;
|
|
646
|
+
* 但历史 commit 已 track 的 `private/*.md` 仍会被 commit,需显式 untrack。
|
|
647
|
+
*
|
|
648
|
+
* **重要副作用(比 `.co-engram/` 更危险):** private engram 是用户私人记忆,
|
|
649
|
+
* commit 描述是"删除这些文件"。协作者 pull 时,他们本地 `private/*.md` 也会被
|
|
650
|
+
* git 删除 —— 而 private 通常**不可再生**(用户的个人凭据/路径/偏好)。
|
|
651
|
+
*
|
|
652
|
+
* 调用方必须:
|
|
653
|
+
* 1. dryRun 预览,明确告知"会从 git 移除 N 个 private 文件";
|
|
654
|
+
* 2. 让用户显式 opt-in(不能默认执行);
|
|
655
|
+
* 3. 提示用户:本机磁盘文件保留,但跨机历史已被抹去。
|
|
656
|
+
*
|
|
657
|
+
* @returns 移除的文件数(0 = 本来就没 track 或失败)。
|
|
658
|
+
*/
|
|
659
|
+
export function untrackPrivateDir(repoPath) {
|
|
660
|
+
let trackedRaw = "";
|
|
661
|
+
try {
|
|
662
|
+
trackedRaw = execSync("git ls-files private/", {
|
|
663
|
+
cwd: repoPath,
|
|
664
|
+
stdio: "pipe",
|
|
665
|
+
encoding: "utf8",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
const tracked = trackedRaw
|
|
672
|
+
.split("\n")
|
|
673
|
+
.map((l) => l.trim())
|
|
674
|
+
.filter(Boolean);
|
|
675
|
+
if (tracked.length === 0) {
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
execSync("git rm -r --cached --quiet private/", {
|
|
680
|
+
cwd: repoPath,
|
|
681
|
+
stdio: "pipe",
|
|
682
|
+
encoding: "utf8",
|
|
683
|
+
});
|
|
684
|
+
return tracked.length;
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 只读检测:`private/` 下已 tracked 的文件数(不修改 index)。
|
|
692
|
+
*
|
|
693
|
+
* 用于 dryRun 预测 + 工具层判断是否需要提示用户 opt-in untrack。
|
|
694
|
+
*/
|
|
695
|
+
export function countTrackedPrivateDir(repoPath) {
|
|
696
|
+
try {
|
|
697
|
+
const raw = execSync("git ls-files private/", {
|
|
698
|
+
cwd: repoPath,
|
|
699
|
+
stdio: "pipe",
|
|
700
|
+
encoding: "utf8",
|
|
701
|
+
});
|
|
702
|
+
return raw
|
|
703
|
+
.split("\n")
|
|
704
|
+
.map((l) => l.trim())
|
|
705
|
+
.filter(Boolean).length;
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return 0;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* 读取 .gitignore 内容(用于诊断/展示)。不存在返回 null。
|
|
713
|
+
*/
|
|
714
|
+
export function readGitignore(repoPath) {
|
|
715
|
+
const gitignorePath = join(repoPath, ".gitignore");
|
|
716
|
+
if (!existsSync(gitignorePath))
|
|
717
|
+
return null;
|
|
718
|
+
try {
|
|
719
|
+
return readFileSync(gitignorePath, "utf8");
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* 只读检测:.gitignore 是否已含指定 rule(逐行 trim 后比对)。
|
|
727
|
+
*
|
|
728
|
+
* 与 `appendToGitignore` 的检测逻辑镜像,但不写盘。
|
|
729
|
+
* 用于 dryRun 预测:engram_sync 在只读模式下判断是否需要追加 private/。
|
|
730
|
+
*/
|
|
731
|
+
export function gitignoreContainsRule(repoPath, rule) {
|
|
732
|
+
const content = readGitignore(repoPath);
|
|
733
|
+
if (!content)
|
|
734
|
+
return false;
|
|
735
|
+
return content
|
|
736
|
+
.split(/\r?\n/)
|
|
737
|
+
.some((line) => line.trim() === rule.trim());
|
|
738
|
+
}
|
|
156
739
|
//# sourceMappingURL=git.js.map
|