@sanqianx/project-knowledge 4.7.0 → 5.0.0
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/.agents/plugins/marketplace.json +17 -17
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +836 -799
- package/README.md +58 -41
- package/_modules/knowledge-engine/README.md +91 -0
- package/_modules/knowledge-engine/package.json +38 -0
- package/_modules/knowledge-engine/src/analysis-contract.js +25 -0
- package/_modules/knowledge-engine/src/analysis-worker.js +54 -0
- package/_modules/knowledge-engine/src/bin.js +52 -0
- package/_modules/knowledge-engine/src/bridge-client.js +24 -0
- package/_modules/knowledge-engine/src/coordinator.js +263 -0
- package/_modules/knowledge-engine/src/headless-analyzer.js +75 -0
- package/_modules/knowledge-engine/src/memories.js +85 -0
- package/_modules/knowledge-engine/src/retrieval.js +116 -0
- package/_modules/knowledge-engine/src/runtime/README.md +7 -0
- package/_modules/knowledge-engine/src/runtime/atomic-file.js +204 -0
- package/_modules/knowledge-engine/src/runtime/automation-config.js +140 -0
- package/_modules/knowledge-engine/src/runtime/commit-conversation-binder.js +249 -0
- package/_modules/knowledge-engine/src/runtime/commit-processing-ledger.js +26 -0
- package/_modules/knowledge-engine/src/runtime/commit-prompt.js +202 -0
- package/_modules/knowledge-engine/src/runtime/commit-reconciler.js +432 -0
- package/_modules/knowledge-engine/src/runtime/content-hash.js +4 -0
- package/_modules/knowledge-engine/src/runtime/contracts.js +279 -0
- package/_modules/knowledge-engine/src/runtime/conversation-exclusions.js +204 -0
- package/_modules/knowledge-engine/src/runtime/conversation-store.js +387 -0
- package/_modules/knowledge-engine/src/runtime/evidence-bundle.js +228 -0
- package/_modules/knowledge-engine/src/runtime/git-runner.js +62 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-promotion.js +444 -0
- package/_modules/knowledge-engine/src/runtime/knowledge-retrieval-service.js +402 -0
- package/_modules/knowledge-engine/src/runtime/layout.js +44 -0
- package/_modules/knowledge-engine/src/runtime/markdown-knowledge-indexer.js +230 -0
- package/_modules/knowledge-engine/src/runtime/scanner.js +298 -0
- package/_modules/knowledge-engine/src/server.js +154 -0
- package/_modules/knowledge-engine/src/store.js +236 -0
- package/_modules/knowledge-engine/ui/app.css +172 -0
- package/_modules/knowledge-engine/ui/app.js +297 -0
- package/_modules/knowledge-engine/ui/index.html +95 -0
- package/_site/README.md +30 -30
- package/_site/_test/ai-profile-resolver-test.js +137 -137
- package/_site/_test/automation-queue-test.js +14 -14
- package/_site/_test/automation-ui-test.js +40 -40
- package/_site/_test/background-task-registry-test.js +43 -43
- package/_site/_test/baseline-schema-test.js +91 -91
- package/_site/_test/chat-claudecodeui-match-test.js +92 -92
- package/_site/_test/claude-executable-discovery-test.js +62 -62
- package/_site/_test/claude-workbench-test.js +170 -170
- package/_site/_test/codex-conversation-projection-test.js +2 -1
- package/_site/_test/commit-conversation-binding-test.js +95 -95
- package/_site/_test/commit-evidence-test.js +110 -110
- package/_site/_test/conversation-api-test.js +109 -109
- package/_site/_test/conversation-store-test.js +136 -136
- package/_site/_test/desktop-browser-compat-test.js +28 -28
- package/_site/_test/desktop-hook-runtime-regression-test.js +204 -204
- package/_site/_test/explicit-commit-processor-test.js +37 -37
- package/_site/_test/fixtures/make-git-repos.js +12 -1
- package/_site/_test/folder-picker-output-test.js +19 -19
- package/_site/_test/fresh-hook-reimport-test.js +53 -0
- package/_site/_test/fresh-product-data-test.js +42 -0
- package/_site/_test/full-integration-e2e-test.js +165 -165
- package/_site/_test/git-validation-test.js +2 -2
- package/_site/_test/hook-runtime-endpoint-test.js +88 -88
- package/_site/_test/hook-status-repair-api-test.js +152 -152
- package/_site/_test/hook-trigger-test.js +85 -85
- package/_site/_test/import-preflight-api-test.js +234 -233
- package/_site/_test/index-writer-concurrency-test.js +135 -135
- package/_site/_test/integration-adapters-test.js +171 -171
- package/_site/_test/integration-surface-coverage-test.js +164 -164
- package/_site/_test/knowledge-language-control-test.js +170 -170
- package/_site/_test/knowledge-promotion-recovery-test.js +260 -260
- package/_site/_test/knowledge-query-test.js +54 -54
- package/_site/_test/knowledge-retrieval-service-test.js +77 -77
- package/_site/_test/knowledge-storage-startup-test.js +52 -52
- package/_site/_test/knowledge-store-logs-supervision-test.js +89 -89
- package/_site/_test/logging-api-test.js +89 -89
- package/_site/_test/logging-sse-no-gap-test.js +88 -88
- package/_site/_test/logging-ui-test.js +106 -104
- package/_site/_test/markdown-delta-overlay-test.js +85 -85
- package/_site/_test/markdown-maintenance-api-test.js +75 -75
- package/_site/_test/mcp-server-test.js +149 -149
- package/_site/_test/module-artifact-integrity-test.js +27 -0
- package/_site/_test/module-boundary-test.js +30 -0
- package/_site/_test/module-bridge-eventbridge-test.js +7 -0
- package/_site/_test/module-model-configuration-test.js +44 -0
- package/_site/_test/module-process-lifecycle-test.js +24 -0
- package/_site/_test/module-stream-proxy-test.js +34 -0
- package/_site/_test/non-release-ci-test.js +34 -34
- package/_site/_test/offline-boundary-isolation-test.js +32 -32
- package/_site/_test/packaged-ui-smoke-test.js +11 -9
- package/_site/_test/path-consistency-test.js +151 -151
- package/_site/_test/pending-sweep-test.js +9 -9
- package/_site/_test/post-commit-automation-test.js +87 -87
- package/_site/_test/product-diagnostics-ui-test.js +75 -0
- package/_site/_test/product-import-ui-test.js +92 -0
- package/_site/_test/project-delete-recovery-test.js +64 -64
- package/_site/_test/project-goal-editor-test.js +144 -144
- package/_site/_test/project-lifecycle-transaction-test.js +106 -106
- package/_site/_test/prompt-settings-test.js +115 -115
- package/_site/_test/protected-architecture-gate-test.js +122 -122
- package/_site/_test/refactor-characterization-test.js +36 -36
- package/_site/_test/release-version-sync-test.js +1 -1
- package/_site/_test/requirement-recorder-test.js +173 -173
- package/_site/_test/run-all-tests.js +159 -156
- package/_site/_test/shared-contracts-test.js +58 -58
- package/_site/_test/startup-analysis-disabled-test.js +10 -10
- package/_site/_test/storage-foundation-test.js +78 -78
- package/_site/_test/structured-logger-test.js +101 -101
- package/_site/_test/tracking-start-test.js +109 -108
- package/_site/_test/workbench-permission-test.js +117 -117
- package/_site/_test/workspace-ui-contract-test.js +28 -43
- package/_site/lib/ai-profile-resolver.js +78 -78
- package/_site/lib/ai-workspace.js +101 -101
- package/_site/lib/automation-config.js +140 -140
- package/_site/lib/bridge-adapter.js +1 -0
- package/_site/lib/bridge-consumer-service.js +402 -402
- package/_site/lib/claude-cli-runner.js +1510 -1510
- package/_site/lib/commit-conversation-binder.js +234 -234
- package/_site/lib/commit-processing-ledger.js +27 -27
- package/_site/lib/commit-prompt.js +202 -202
- package/_site/lib/commit-reconciler.js +406 -406
- package/_site/lib/contracts.js +284 -279
- package/_site/lib/conversation-query-service.js +144 -144
- package/_site/lib/conversation-store.js +385 -385
- package/_site/lib/data-dir.js +6 -74
- package/_site/lib/engine-connection.js +67 -0
- package/_site/lib/evidence-bundle.js +228 -228
- package/_site/lib/folder-picker-output.js +21 -21
- package/_site/lib/git-runner.js +62 -62
- package/_site/lib/github-team-store.js +1172 -1172
- package/_site/lib/hook-manager.js +203 -202
- package/_site/lib/index-service.js +173 -173
- package/_site/lib/integration-installer.js +908 -907
- package/_site/lib/kb-framework.js +176 -176
- package/_site/lib/kb-validator.js +124 -124
- package/_site/lib/knowledge-promotion.js +435 -435
- package/_site/lib/knowledge-retrieval-service.js +401 -401
- package/_site/lib/knowledge-tool-runtime.js +359 -359
- package/_site/lib/llm-client.js +161 -161
- package/_site/lib/markdown-knowledge-indexer.js +230 -230
- package/_site/lib/module-bridge.js +191 -36
- package/_site/lib/post-commit-automation.js +87 -87
- package/_site/lib/product-data.js +24 -0
- package/_site/lib/project-lifecycle-service.js +535 -497
- package/_site/lib/project-store.js +268 -262
- package/_site/lib/requirement-recorder.js +278 -278
- package/_site/lib/runtime-endpoint.js +155 -155
- package/_site/lib/scanner.js +298 -298
- package/_site/lib/server-app.js +1579 -1520
- package/_site/lib/settings-store.js +133 -113
- package/_site/lib/storage-layout.js +162 -162
- package/_site/lib/structured-logger.js +574 -574
- package/_site/scripts/folder-picker.ps1 +156 -156
- package/_site/scripts/hook-trigger.js +157 -156
- package/_site/scripts/install-module-candidates.js +69 -0
- package/_site/scripts/pack-module-candidates.js +72 -0
- package/_site/scripts/sync-release-version.js +2 -2
- package/_site/scripts/vendor-modules.js +47 -0
- package/_site/scripts/verify-module-runtime.js +45 -0
- package/_site/scripts/verify-product-runtime.js +34 -0
- package/bin/project-knowledge-mcp.js +194 -194
- package/docs/README.zh-CN.md +42 -29
- package/docs/fresh-v5-start.md +28 -0
- package/docs/project-registry-schema.md +22 -22
- package/docs/testing-strategy.md +33 -33
- package/module-runtime-manifest.json +220 -0
- package/package.json +14 -9
- package/plugins/project-knowledge/.claude-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.codex-plugin/plugin.json +1 -1
- package/plugins/project-knowledge/.mcp.json +1 -1
- package/plugins/project-knowledge/opencode/project-knowledge.md +3 -3
- package/plugins/project-knowledge/skills/project-knowledge/SKILL.md +28 -28
- package/templates/project-readme.md +34 -34
- package/ui/favicon.svg +38 -38
- package/ui/index.html +52 -149
- package/ui/product.css +9 -0
- package/ui/product.js +412 -0
- package/vendor-manifest.json +45 -0
- package/_site/_test/data-dir-migration-test.js +0 -77
- package/_site/_test/import-ui-flow-test.js +0 -171
- package/_site/_test/knowledge-migration-test.js +0 -75
- package/_site/_test/legacy-forward-compat-test.js +0 -257
- package/_site/_test/legacy-project-upgrade-e2e-test.js +0 -363
- package/_site/_test/p0-data-migration-characterization-test.js +0 -37
- package/_site/_test/p0-e2e-gate-test.js +0 -347
- package/_site/_test/project-control-panel-task14-test.js +0 -63
- package/_site/_test/project-layout-v2-migration-test.js +0 -127
- package/_site/_test/server-runtime-migration-safety-test.js +0 -27
- package/_site/_test/task15-20-ui-flow-test.js +0 -148
- package/_site/_test/ui-i18n-toggle-test.js +0 -114
- package/_site/_test/ui-smoke-test.js +0 -73
- package/_site/_test/v4122-upgrade-data-contract-test.js +0 -56
- package/_site/lib/data-state-classifier.js +0 -40
- package/_site/lib/legacy-data-manifest.js +0 -26
- package/_site/lib/migration-service.js +0 -442
- package/ui/app.css +0 -58
- package/ui/app.js +0 -607
- package/ui/i18n.js +0 -146
|
@@ -1,202 +1,203 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const { spawnSync } = require('child_process');
|
|
4
|
-
const AtomicFile = require('./atomic-file');
|
|
5
|
-
const { DomainError, validateProjectId } = require('./contracts');
|
|
6
|
-
|
|
7
|
-
const HOOK_NAME = 'post-commit';
|
|
8
|
-
const HOOK_MARKER = '# PROJECT-KNOWLEDGE-HOOK ';
|
|
9
|
-
const LEGACY_HOOK_MARKER = '# KB-HOOK-MANAGED v1';
|
|
10
|
-
const MANAGED_VERSION = 2;
|
|
11
|
-
const HOOK_SCHEMA = 'project-knowledge/hook/v2';
|
|
12
|
-
|
|
13
|
-
function runGit(repoPath, args) {
|
|
14
|
-
const result = spawnSync('git', ['-C', repoPath, ...args], {
|
|
15
|
-
encoding: 'utf8',
|
|
16
|
-
windowsHide: true,
|
|
17
|
-
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
18
|
-
});
|
|
19
|
-
if (result.status !== 0) {
|
|
20
|
-
throw new DomainError('HOOK_INVALID', 'Git could not resolve the repository hook path.', {
|
|
21
|
-
status: 400,
|
|
22
|
-
details: { gitExitCode: result.status, gitError: String(result.stderr || '').trim().slice(0, 1000) },
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
return String(result.stdout || '').trim();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function resolveGitHooksDir(_gitDir, repoPath) {
|
|
29
|
-
if (!repoPath) throw new DomainError('INVALID_ARGUMENT', 'repoPath is required.');
|
|
30
|
-
const absolute = path.resolve(repoPath);
|
|
31
|
-
const hooksPath = runGit(absolute, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
|
|
32
|
-
if (!hooksPath) throw new DomainError('HOOK_INVALID', 'Git returned an empty hooks path.', { status: 400 });
|
|
33
|
-
return path.resolve(absolute, hooksPath);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function repoGitDir(repoPath) {
|
|
37
|
-
try { return runGit(path.resolve(repoPath), ['rev-parse', '--path-format=absolute', '--git-dir']); }
|
|
38
|
-
catch { return null; }
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function shellQuote(value) { return `'${String(value).replace(/'/g, `'"'"'`)}'`; }
|
|
42
|
-
function toGitBashPath(value) { return path.resolve(value).replace(/\\/g, '/'); }
|
|
43
|
-
|
|
44
|
-
function markerFor(projectId) {
|
|
45
|
-
return `${HOOK_MARKER}${JSON.stringify({ schema: HOOK_SCHEMA, managedVersion: MANAGED_VERSION, projectId })}`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function parseManagedMarker(text) {
|
|
49
|
-
const line = String(text || '').split(/\r?\n/).find(item => item.startsWith(HOOK_MARKER));
|
|
50
|
-
if (!line) return null;
|
|
51
|
-
try {
|
|
52
|
-
const marker = JSON.parse(line.slice(HOOK_MARKER.length));
|
|
53
|
-
if (marker.schema !== HOOK_SCHEMA || marker.managedVersion !== MANAGED_VERSION) return null;
|
|
54
|
-
validateProjectId(marker.projectId);
|
|
55
|
-
return marker;
|
|
56
|
-
} catch { return null; }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function isLegacyManagedHook(text) {
|
|
60
|
-
return String(text || '').split(/\r?\n/).some(line => line.startsWith(LEGACY_HOOK_MARKER));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function buildHookBody({ projectId, triggerScriptPath, nodeExecutable = process.execPath }) {
|
|
64
|
-
validateProjectId(projectId);
|
|
65
|
-
if (!triggerScriptPath) throw new DomainError('HOOK_INVALID', 'triggerScriptPath is required.', { status: 400 });
|
|
66
|
-
const trigger = path.resolve(triggerScriptPath);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
--
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
const AtomicFile = require('./atomic-file');
|
|
5
|
+
const { DomainError, validateProjectId } = require('./contracts');
|
|
6
|
+
|
|
7
|
+
const HOOK_NAME = 'post-commit';
|
|
8
|
+
const HOOK_MARKER = '# PROJECT-KNOWLEDGE-HOOK ';
|
|
9
|
+
const LEGACY_HOOK_MARKER = '# KB-HOOK-MANAGED v1';
|
|
10
|
+
const MANAGED_VERSION = 2;
|
|
11
|
+
const HOOK_SCHEMA = 'project-knowledge/hook/v2';
|
|
12
|
+
|
|
13
|
+
function runGit(repoPath, args) {
|
|
14
|
+
const result = spawnSync('git', ['-C', repoPath, ...args], {
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
18
|
+
});
|
|
19
|
+
if (result.status !== 0) {
|
|
20
|
+
throw new DomainError('HOOK_INVALID', 'Git could not resolve the repository hook path.', {
|
|
21
|
+
status: 400,
|
|
22
|
+
details: { gitExitCode: result.status, gitError: String(result.stderr || '').trim().slice(0, 1000) },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return String(result.stdout || '').trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveGitHooksDir(_gitDir, repoPath) {
|
|
29
|
+
if (!repoPath) throw new DomainError('INVALID_ARGUMENT', 'repoPath is required.');
|
|
30
|
+
const absolute = path.resolve(repoPath);
|
|
31
|
+
const hooksPath = runGit(absolute, ['rev-parse', '--path-format=absolute', '--git-path', 'hooks']);
|
|
32
|
+
if (!hooksPath) throw new DomainError('HOOK_INVALID', 'Git returned an empty hooks path.', { status: 400 });
|
|
33
|
+
return path.resolve(absolute, hooksPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function repoGitDir(repoPath) {
|
|
37
|
+
try { return runGit(path.resolve(repoPath), ['rev-parse', '--path-format=absolute', '--git-dir']); }
|
|
38
|
+
catch { return null; }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shellQuote(value) { return `'${String(value).replace(/'/g, `'"'"'`)}'`; }
|
|
42
|
+
function toGitBashPath(value) { return path.resolve(value).replace(/\\/g, '/'); }
|
|
43
|
+
|
|
44
|
+
function markerFor(projectId) {
|
|
45
|
+
return `${HOOK_MARKER}${JSON.stringify({ schema: HOOK_SCHEMA, managedVersion: MANAGED_VERSION, projectId })}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseManagedMarker(text) {
|
|
49
|
+
const line = String(text || '').split(/\r?\n/).find(item => item.startsWith(HOOK_MARKER));
|
|
50
|
+
if (!line) return null;
|
|
51
|
+
try {
|
|
52
|
+
const marker = JSON.parse(line.slice(HOOK_MARKER.length));
|
|
53
|
+
if (marker.schema !== HOOK_SCHEMA || marker.managedVersion !== MANAGED_VERSION) return null;
|
|
54
|
+
validateProjectId(marker.projectId);
|
|
55
|
+
return marker;
|
|
56
|
+
} catch { return null; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isLegacyManagedHook(text) {
|
|
60
|
+
return String(text || '').split(/\r?\n/).some(line => line.startsWith(LEGACY_HOOK_MARKER));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildHookBody({ projectId, triggerScriptPath, nodeExecutable = process.execPath, dataDir, bridgeHomeDir, bridgeModulePath }) {
|
|
64
|
+
validateProjectId(projectId);
|
|
65
|
+
if (!triggerScriptPath) throw new DomainError('HOOK_INVALID', 'triggerScriptPath is required.', { status: 400 });
|
|
66
|
+
const trigger = path.resolve(triggerScriptPath);
|
|
67
|
+
const binding = [[dataDir && '--data-dir', dataDir], [bridgeHomeDir && '--bridge-home', bridgeHomeDir], [bridgeModulePath && '--bridge-module', bridgeModulePath]].filter(([flag]) => flag).map(([flag, value]) => ` ${flag} ${shellQuote(value)}`).join('');
|
|
68
|
+
if (!fs.existsSync(trigger) || !fs.statSync(trigger).isFile()) {
|
|
69
|
+
throw new DomainError('HOOK_INVALID', 'Hook trigger script does not exist.', { status: 400 });
|
|
70
|
+
}
|
|
71
|
+
// T02 runtime contract: Git invokes the hook from a normal shell, which does
|
|
72
|
+
// NOT inherit ELECTRON_RUN_AS_NODE=1 the way the Desktop backend does.
|
|
73
|
+
// Without this prefix, the Electron executable (when used as nodeExecutable
|
|
74
|
+
// in a packaged Desktop install) would launch as a GUI product instead of
|
|
75
|
+
// executing the trigger as Node. Pure Node ignores the env var, so the
|
|
76
|
+
// CLI install path is unaffected.
|
|
77
|
+
return `#!/bin/sh
|
|
78
|
+
${markerFor(projectId)}
|
|
79
|
+
# Generated by project-knowledge. It captures a durable boundary before best-effort notification.
|
|
80
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
81
|
+
ELECTRON_RUN_AS_NODE=1 ${shellQuote(toGitBashPath(nodeExecutable))} ${shellQuote(toGitBashPath(trigger))} \\
|
|
82
|
+
--project-id ${shellQuote(projectId)} \\
|
|
83
|
+
--repo-root "$REPO_ROOT"${binding} >/dev/null 2>&1 || true
|
|
84
|
+
exit 0
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hookPathForRepo(repoPath) {
|
|
89
|
+
const absolute = path.resolve(repoPath);
|
|
90
|
+
const gitDir = repoGitDir(absolute);
|
|
91
|
+
if (!gitDir) throw new DomainError('HOOK_INVALID', 'The project is not a Git repository.', { status: 400 });
|
|
92
|
+
return path.join(resolveGitHooksDir(gitDir, absolute), HOOK_NAME);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function installHook(options = {}) {
|
|
96
|
+
const repoPath = path.resolve(String(options.repoPath || ''));
|
|
97
|
+
const projectId = validateProjectId(options.projectId || options.projectSlug || '');
|
|
98
|
+
const triggerScriptPath = options.triggerScriptPath || (options.siteRoot ? path.join(options.siteRoot, 'scripts', 'hook-trigger.js') : '');
|
|
99
|
+
const body = buildHookBody({ ...options, projectId, triggerScriptPath, nodeExecutable: options.nodeExecutable });
|
|
100
|
+
const hookPath = hookPathForRepo(repoPath);
|
|
101
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
102
|
+
if (fs.existsSync(hookPath)) {
|
|
103
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
104
|
+
const marker = parseManagedMarker(existing);
|
|
105
|
+
if (!marker) {
|
|
106
|
+
if (!isLegacyManagedHook(existing)) {
|
|
107
|
+
throw new DomainError('HOOK_CONFLICT', 'A non-managed post-commit hook already exists.', { status: 409 });
|
|
108
|
+
}
|
|
109
|
+
// Legacy v1 managed hook: preflight already classifies it as an
|
|
110
|
+
// in-place upgrade target ("legacy-v1"), so overwrite below instead of
|
|
111
|
+
// refusing — otherwise import promises a migration the installer
|
|
112
|
+
// rejects for every repo hooked by an older KB generation.
|
|
113
|
+
}
|
|
114
|
+
if (marker && marker.projectId !== projectId && options.replaceManagedHookProjectId !== marker.projectId) {
|
|
115
|
+
throw new DomainError('HOOK_CONFLICT', 'The managed hook belongs to another project.', { status: 409 });
|
|
116
|
+
}
|
|
117
|
+
if (existing === body) return { ok: true, installed: true, updated: false, hookPath, projectId, managedVersion: MANAGED_VERSION };
|
|
118
|
+
}
|
|
119
|
+
AtomicFile.writeFileAtomic(hookPath, body, { mode: 0o755 });
|
|
120
|
+
try { fs.chmodSync(hookPath, 0o755); } catch {
|
|
121
|
+
// Windows does not expose POSIX executable bits; the atomic write used mode 0755 where supported.
|
|
122
|
+
}
|
|
123
|
+
const readback = fs.readFileSync(hookPath, 'utf8');
|
|
124
|
+
const marker = parseManagedMarker(readback);
|
|
125
|
+
if (!marker || marker.projectId !== projectId || !readback.includes(toGitBashPath(triggerScriptPath))) {
|
|
126
|
+
try { fs.unlinkSync(hookPath); } catch {
|
|
127
|
+
// Verification failure remains authoritative; the lifecycle journal records rollback residue.
|
|
128
|
+
}
|
|
129
|
+
throw new DomainError('HOOK_INVALID', 'Managed hook verification failed.', { status: 500 });
|
|
130
|
+
}
|
|
131
|
+
return { ok: true, installed: true, updated: true, hookPath, projectId, managedVersion: MANAGED_VERSION };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function uninstallHook({ repoPath, projectId = '' } = {}) {
|
|
135
|
+
const absolute = path.resolve(String(repoPath || ''));
|
|
136
|
+
const hookPath = hookPathForRepo(absolute);
|
|
137
|
+
if (!fs.existsSync(hookPath)) return { ok: true, removed: false, hookPath };
|
|
138
|
+
const text = fs.readFileSync(hookPath, 'utf8');
|
|
139
|
+
const marker = parseManagedMarker(text);
|
|
140
|
+
if (!marker) throw new DomainError('HOOK_CONFLICT', 'The post-commit hook is not managed by project-knowledge.', { status: 409 });
|
|
141
|
+
if (projectId && marker.projectId !== validateProjectId(projectId)) throw new DomainError('HOOK_CONFLICT', 'The managed hook belongs to another project.', { status: 409 });
|
|
142
|
+
fs.unlinkSync(hookPath);
|
|
143
|
+
return { ok: true, removed: true, hookPath, projectId: marker.projectId };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readHookStatus({ repoPath, projectId = '' } = {}) {
|
|
147
|
+
const absolute = path.resolve(String(repoPath || ''));
|
|
148
|
+
let hookPath;
|
|
149
|
+
try { hookPath = hookPathForRepo(absolute); }
|
|
150
|
+
catch (error) { return { ok: true, installed: false, repoPath: absolute, reason: error.message }; }
|
|
151
|
+
if (!fs.existsSync(hookPath)) return { ok: true, installed: false, hookPath, repoPath: absolute };
|
|
152
|
+
const text = fs.readFileSync(hookPath, 'utf8');
|
|
153
|
+
const marker = parseManagedMarker(text);
|
|
154
|
+
if (!marker) return { ok: true, installed: false, kbManaged: false, hookPath, repoPath: absolute, reason: isLegacyManagedHook(text) ? 'legacy managed hook requires migration' : 'third-party hook' };
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
installed: !projectId || marker.projectId === projectId,
|
|
158
|
+
kbManaged: true,
|
|
159
|
+
hookPath,
|
|
160
|
+
repoPath: absolute,
|
|
161
|
+
projectId: marker.projectId,
|
|
162
|
+
managedVersion: marker.managedVersion,
|
|
163
|
+
bytes: text.length,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function migrateManagedHook(options = {}) {
|
|
168
|
+
const hookPath = hookPathForRepo(options.repoPath);
|
|
169
|
+
if (!fs.existsSync(hookPath)) return { ok: true, migrated: false, reason: 'missing' };
|
|
170
|
+
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
171
|
+
const current = parseManagedMarker(existing);
|
|
172
|
+
if (current) return { ok: true, migrated: false, reason: 'current', hookPath, managedVersion: current.managedVersion };
|
|
173
|
+
if (!isLegacyManagedHook(existing)) throw new DomainError('HOOK_CONFLICT', 'A third-party post-commit hook cannot be migrated.', { status: 409 });
|
|
174
|
+
const result = installLegacyReplacement(options, hookPath);
|
|
175
|
+
return { ...result, migrated: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function installLegacyReplacement(options, hookPath) {
|
|
179
|
+
const body = buildHookBody(options);
|
|
180
|
+
AtomicFile.writeFileAtomic(hookPath, body, { mode: 0o755 });
|
|
181
|
+
try { fs.chmodSync(hookPath, 0o755); } catch {
|
|
182
|
+
// Windows does not expose POSIX executable bits; the atomic write used mode 0755 where supported.
|
|
183
|
+
}
|
|
184
|
+
const marker = parseManagedMarker(fs.readFileSync(hookPath, 'utf8'));
|
|
185
|
+
if (!marker) throw new DomainError('HOOK_INVALID', 'Migrated hook verification failed.', { status: 500 });
|
|
186
|
+
return { ok: true, installed: true, updated: true, hookPath, projectId: marker.projectId, managedVersion: marker.managedVersion };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
HOOK_NAME,
|
|
191
|
+
HOOK_MARKER,
|
|
192
|
+
LEGACY_HOOK_MARKER,
|
|
193
|
+
MANAGED_VERSION,
|
|
194
|
+
HOOK_SCHEMA,
|
|
195
|
+
resolveGitHooksDir,
|
|
196
|
+
repoGitDir,
|
|
197
|
+
parseManagedMarker,
|
|
198
|
+
buildHookBody,
|
|
199
|
+
installHook,
|
|
200
|
+
uninstallHook,
|
|
201
|
+
readHookStatus,
|
|
202
|
+
migrateManagedHook,
|
|
203
|
+
};
|