wendkeep 0.75.3 → 0.76.1
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/CHANGELOG.md +33 -0
- package/README.en.md +4 -1
- package/README.md +4 -1
- package/docs/en/commands/context.md +88 -0
- package/docs/en/commands/getting-started.md +3 -1
- package/docs/en/commands/maintenance-and-diagnostics.md +3 -0
- package/docs/en/commands/worktrees.md +93 -0
- package/docs/pt-BR/commands/context.md +86 -0
- package/docs/pt-BR/commands/getting-started.md +3 -1
- package/docs/pt-BR/commands/maintenance-and-diagnostics.md +2 -0
- package/docs/pt-BR/commands/worktrees.md +91 -0
- package/hooks/flow-protected-policy.mjs +1 -1
- package/hooks/project-scope.mjs +51 -6
- package/package.json +2 -1
- package/packages/cli/src/index.mjs +25 -3
- package/packages/vault/src/index.mjs +1 -0
- package/packages/vault/src/project-vault.mjs +25 -2
- package/packages/vault/src/worktree-metadata.mjs +274 -0
- package/src/context.mjs +297 -0
- package/src/doctor.mjs +8 -0
- package/src/init.mjs +6 -0
- package/src/sync.mjs +4 -1
- package/src/worktree.mjs +708 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wendkeep",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.1",
|
|
4
4
|
"description": "Vault-first persistent memory for AI coding agents, with an optional profile-aware governance runtime: OFF, FLOW, GUIDE, GOVERN, or ASSURE. Local-first and agent-agnostic (Claude Code, Codex, Cursor…).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
|
+
"precheck": "node --check src/worktree.mjs && node --check src/context.mjs && node --check packages/vault/src/worktree-metadata.mjs",
|
|
44
45
|
"check": "node --check scripts/release.mjs && node --check scripts/release-plan.mjs && node --check scripts/release-provenance.mjs && node --check scripts/run-scope.mjs && node --check src/release-provenance.mjs && node --check bin/wendkeep.mjs && node --check packages/cli/src/index.mjs && node --check src/init.mjs && node --check src/doctor.mjs && node --check src/project-vault.mjs && node --check src/observer-auth.mjs && node --check src/observer-privacy.mjs && node --check src/observer-snapshot.mjs && node --check src/observer-store.mjs && node --check src/observer-memory.mjs && node --check src/observer-memory-publish.mjs && node --check src/observer-sql-store.mjs && node --check src/observer-sql-migrate.mjs && node --check src/observer-sql-publish.mjs && node --check src/observer-transcript-store.mjs && node --check src/observer-server.mjs && node --check src/observer.mjs && node --check src/observer-publish.mjs && node --check src/operating-profile.mjs && node --check src/profile.mjs && node --check src/flow.mjs && node --check src/work-kind.mjs && node --check src/delivery.mjs && node --check web/observer/app.mjs && node --check hooks/observer-publish.mjs && node --check hooks/evidence-context.mjs && node --check hooks/evidence-recall.mjs && node --check hooks/memory-scope.mjs && node --check hooks/operating-profile-runtime.mjs && node --check hooks/operating-profile-task-store.mjs && node --check hooks/flow-core.mjs && node --check hooks/flow-protected-policy.mjs && node --check hooks/git-snapshot.mjs && node --check hooks/vault-path-safety.mjs && node --check hooks/vault-runtime-store.mjs && node --check packages/harness/src/index.mjs && node --check packages/harness/src/flow-store.mjs && node --check packages/harness/src/operating-profile.mjs && node --check packages/harness/src/sensors-core.mjs && node --check packages/integrations/src/host-hooks.mjs && node --check packages/integrations/src/hook-envelope.mjs && node --check packages/integrations/src/prompt-content.mjs && node --check packages/integrations/src/transcript-usage.mjs && node --check packages/integrations/src/transcripts.mjs && node --check packages/integrations/src/session-identity.mjs && node --check packages/integrations/src/index.mjs && node --check packages/mcp/src/config.mjs && node --check packages/mcp/src/index.mjs && node --check packages/vault/src/index.mjs && node --check packages/vault/src/project-vault.mjs && node --check packages/vault/src/vault-path-safety.mjs && node --check packages/vault/src/locale.mjs && node --check packages/vault/src/memory-schema.mjs && node --check packages/vault/src/memory-mode.mjs && node --check packages/vault/src/memory-scope.mjs && node --check packages/vault/src/memory-candidate-policy.mjs && node --check packages/vault/src/evidence-recall.mjs && node --check packages/vault/src/memory-handoff.mjs && node --check packages/vault/src/memory-store.mjs && node --check packages/vault/src/validate-core.mjs && node --check packages/vault/src/validate-memory.mjs",
|
|
45
46
|
"test": "node --test --test-concurrency=2",
|
|
46
47
|
"test:core": "node scripts/run-scope.mjs core",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// wendkeep CLI — canonical private runtime.
|
|
2
|
-
// wendkeep init [--vault <path>] [--project <path>] [--no-mcp] [--yes] [--force]
|
|
2
|
+
// wendkeep init [--vault <path>] [--project <path>] [--no-mcp] [--yes] [--force] [--vscode-worktree-tasks]
|
|
3
3
|
// wendkeep hook <name> (invoked by the agent's settings.json; pipes stdin/stdout)
|
|
4
4
|
// wendkeep doctor [--vault <path>]
|
|
5
5
|
// wendkeep --version | --help
|
|
@@ -33,6 +33,7 @@ Usage:
|
|
|
33
33
|
(default: none — opt in explicitly). dotcontext is legacy — the native a2 loop replaces it.
|
|
34
34
|
--no-companions Skip companion plugins/MCP entirely.
|
|
35
35
|
--no-colors Skip the Obsidian color system (.obsidian snippet + graph groups).
|
|
36
|
+
--vscode-worktree-tasks Create local, Git-excluded VS Code tasks for managed worktrees.
|
|
36
37
|
--dotcontext-mcp <v> dotcontext MCP placement: auto (default; skip project entry
|
|
37
38
|
if already global), project, or none.
|
|
38
39
|
--dotcontext-hooks <v> dotcontext hooks: full (default), light (no PostToolUse), none.
|
|
@@ -46,10 +47,18 @@ Usage:
|
|
|
46
47
|
command — the three steps that repeat identically after every
|
|
47
48
|
package update. Stops at the first failing step. Install the
|
|
48
49
|
package first (npm i -D wendkeep@latest); a running process
|
|
49
|
-
cannot replace itself. · --vault P · --profile <name> · --yes
|
|
50
|
+
cannot replace itself. · --vault P · --profile <name> · --yes
|
|
51
|
+
· --vscode-worktree-tasks.
|
|
50
52
|
|
|
51
53
|
wendkeep doctor [--vault P] Health check. --scope core|runtime · --strict for CI/release.
|
|
52
54
|
wendkeep observer <sub> Local multi-project Observer: serve | register | publish | reconcile | status.
|
|
55
|
+
wendkeep worktree create <slug> [--base ref] [--branch name] [--open vscode|none] [--json]
|
|
56
|
+
wendkeep worktree list [--json]
|
|
57
|
+
wendkeep worktree status [slug] [--json]
|
|
58
|
+
wendkeep worktree open <slug> [--editor vscode] [--json]
|
|
59
|
+
Managed linked worktrees under .worktrees (branch default wk/<slug>).
|
|
60
|
+
wendkeep context switch <branch> [--create] [--session <id>] [--json]
|
|
61
|
+
Switch Git branch and the causal session scope in the same worktree.
|
|
53
62
|
wendkeep change <sub> Change lifecycle: new [--simple|--guide] | use | bind <slug> --session <id> | continue | list | show |
|
|
54
63
|
status | done <id> | undone <id> | diff | archive [--force] | abandon | relink | backlink.
|
|
55
64
|
archive exige verdict (rode verify --deep); abandon descarta sem ADR.
|
|
@@ -193,6 +202,9 @@ async function main(argv) {
|
|
|
193
202
|
} else if (cmd === 'profile') {
|
|
194
203
|
const { PROFILE_HELP } = await import('../../../src/profile.mjs');
|
|
195
204
|
process.stdout.write(PROFILE_HELP);
|
|
205
|
+
} else if (cmd === 'context') {
|
|
206
|
+
const { CONTEXT_HELP } = await import('../../../src/context.mjs');
|
|
207
|
+
process.stdout.write(CONTEXT_HELP);
|
|
196
208
|
} else {
|
|
197
209
|
process.stdout.write(HELP);
|
|
198
210
|
}
|
|
@@ -206,7 +218,7 @@ async function main(argv) {
|
|
|
206
218
|
// `sync` starts with `init` and resolves the freshly bound Vault itself. Pre-resolving
|
|
207
219
|
// here would prevent that repair step from reporting a corrupt binding as its own
|
|
208
220
|
// first-stage failure (and could never make it as far as the guarded init).
|
|
209
|
-
&& !['init', 'sync', 'hook', 'observer', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
221
|
+
&& !['init', 'sync', 'worktree', 'hook', 'observer', '--version', '-v', '--help', '-h', 'help'].includes(cmd)) {
|
|
210
222
|
await preferProjectVault(rest);
|
|
211
223
|
}
|
|
212
224
|
switch (cmd) {
|
|
@@ -229,6 +241,16 @@ async function main(argv) {
|
|
|
229
241
|
if (rest[0] !== 'serve') process.exit(observerExitCode);
|
|
230
242
|
break;
|
|
231
243
|
}
|
|
244
|
+
case 'worktree': {
|
|
245
|
+
const { runWorktree } = await import('../../../src/worktree.mjs');
|
|
246
|
+
process.exit(runWorktree(rest));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case 'context': {
|
|
250
|
+
const { runContext } = await import('../../../src/context.mjs');
|
|
251
|
+
process.exit(runContext(rest));
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
232
254
|
case 'sync': {
|
|
233
255
|
const { runSync } = await import('../../../src/sync.mjs');
|
|
234
256
|
process.exit(await runSync(rest));
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
writeFileSync,
|
|
10
10
|
} from 'node:fs';
|
|
11
11
|
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
|
|
12
|
+
import { resolveWorktreeVaultBinding } from './worktree-metadata.mjs';
|
|
12
13
|
|
|
13
14
|
export const PROJECT_CONFIG_FILE = '.wendkeep.json';
|
|
14
15
|
export const PROJECT_MARKER_REL = '.brain/PROJECT.json';
|
|
@@ -203,7 +204,7 @@ export function resolveProjectVault({
|
|
|
203
204
|
bindingFailure = error;
|
|
204
205
|
}
|
|
205
206
|
if (binding) {
|
|
206
|
-
|
|
207
|
+
let result = {
|
|
207
208
|
base: binding.base,
|
|
208
209
|
source: 'project-config',
|
|
209
210
|
projectRoot: binding.projectRoot,
|
|
@@ -211,7 +212,29 @@ export function resolveProjectVault({
|
|
|
211
212
|
configPath: binding.configPath,
|
|
212
213
|
config: binding.config,
|
|
213
214
|
};
|
|
214
|
-
if (validateIdentity)
|
|
215
|
+
if (validateIdentity) {
|
|
216
|
+
try {
|
|
217
|
+
validateMarker(result);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const canUsePrivateBinding = error?.code === 'WENDKEEP_VAULT_MARKER_MISSING'
|
|
220
|
+
&& !isAbsolute(binding.config.vault);
|
|
221
|
+
if (!canUsePrivateBinding) throw error;
|
|
222
|
+
const privateBinding = resolveWorktreeVaultBinding({
|
|
223
|
+
startDir: start,
|
|
224
|
+
projectId: binding.config.projectId,
|
|
225
|
+
});
|
|
226
|
+
if (!privateBinding) throw error;
|
|
227
|
+
result = {
|
|
228
|
+
...result,
|
|
229
|
+
base: privateBinding.base,
|
|
230
|
+
source: 'worktree-registry',
|
|
231
|
+
projectRoot: privateBinding.projectRoot,
|
|
232
|
+
repositoryId: privateBinding.repositoryId,
|
|
233
|
+
registryPath: privateBinding.registryPath,
|
|
234
|
+
};
|
|
235
|
+
validateMarker(result);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
215
238
|
return result;
|
|
216
239
|
}
|
|
217
240
|
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import {
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from 'node:fs';
|
|
12
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
13
|
+
|
|
14
|
+
export const WORKTREE_REGISTRY_SCHEMA = 1;
|
|
15
|
+
export const WORKTREE_REGISTRY_REL = 'wendkeep/worktrees-v1.json';
|
|
16
|
+
|
|
17
|
+
function worktreeError(code, message) {
|
|
18
|
+
const error = new Error(message);
|
|
19
|
+
error.code = code;
|
|
20
|
+
return error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function gitOutput(startDir, args, spawn = spawnSync) {
|
|
24
|
+
const result = spawn('git', args, {
|
|
25
|
+
cwd: startDir,
|
|
26
|
+
encoding: 'utf8',
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
});
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
throw worktreeError(
|
|
31
|
+
'WENDKEEP_WORKTREE_GIT_FAILED',
|
|
32
|
+
String(result.stderr || result.error?.message || `git ${args[0]} falhou`).trim(),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return String(result.stdout || '').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function absoluteGitPath(repoRoot, value) {
|
|
39
|
+
return resolve(isAbsolute(value) ? value : join(repoRoot, value));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseWorktreeList(text) {
|
|
43
|
+
const records = [];
|
|
44
|
+
let current = null;
|
|
45
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
46
|
+
if (line.startsWith('worktree ')) {
|
|
47
|
+
current = { path: resolve(line.slice('worktree '.length)) };
|
|
48
|
+
records.push(current);
|
|
49
|
+
} else if (current && line.startsWith('HEAD ')) {
|
|
50
|
+
current.head = line.slice('HEAD '.length);
|
|
51
|
+
} else if (current && line.startsWith('branch ')) {
|
|
52
|
+
current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
|
|
53
|
+
} else if (current && line === 'bare') {
|
|
54
|
+
current.bare = true;
|
|
55
|
+
} else if (current && line === 'detached') {
|
|
56
|
+
current.detached = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return records;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function discoverWorktreeRepository({ startDir = process.cwd(), spawn = spawnSync } = {}) {
|
|
63
|
+
const repoRoot = resolve(gitOutput(startDir, ['rev-parse', '--show-toplevel'], spawn));
|
|
64
|
+
const commonDir = absoluteGitPath(
|
|
65
|
+
repoRoot,
|
|
66
|
+
gitOutput(repoRoot, ['rev-parse', '--git-common-dir'], spawn),
|
|
67
|
+
);
|
|
68
|
+
const gitDir = absoluteGitPath(
|
|
69
|
+
repoRoot,
|
|
70
|
+
gitOutput(repoRoot, ['rev-parse', '--git-dir'], spawn),
|
|
71
|
+
);
|
|
72
|
+
const worktrees = parseWorktreeList(gitOutput(repoRoot, ['worktree', 'list', '--porcelain'], spawn));
|
|
73
|
+
const main = worktrees.find((entry) => !entry.bare) || null;
|
|
74
|
+
if (!main) {
|
|
75
|
+
throw worktreeError(
|
|
76
|
+
'WENDKEEP_WORKTREE_MAIN_UNRESOLVED',
|
|
77
|
+
'Não foi possível localizar a worktree principal do repositório.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
repoRoot,
|
|
82
|
+
commonDir,
|
|
83
|
+
gitDir,
|
|
84
|
+
mainWorktree: main.path,
|
|
85
|
+
worktrees,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sleep(milliseconds) {
|
|
90
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function withWorktreeRegistryLock(registryPath, operation, {
|
|
94
|
+
timeoutMs = 5_000,
|
|
95
|
+
staleMs = 30_000,
|
|
96
|
+
now = () => Date.now(),
|
|
97
|
+
} = {}) {
|
|
98
|
+
const lockPath = `${registryPath}.lock`;
|
|
99
|
+
const startedAt = now();
|
|
100
|
+
mkdirSync(dirname(registryPath), { recursive: true });
|
|
101
|
+
while (true) {
|
|
102
|
+
try {
|
|
103
|
+
mkdirSync(lockPath);
|
|
104
|
+
break;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
107
|
+
try {
|
|
108
|
+
if (now() - statSync(lockPath).mtimeMs > staleMs) {
|
|
109
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
} catch (inspectError) {
|
|
113
|
+
if (inspectError?.code !== 'ENOENT') throw inspectError;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (now() - startedAt >= timeoutMs) {
|
|
117
|
+
throw worktreeError(
|
|
118
|
+
'WENDKEEP_WORKTREE_REGISTRY_BUSY',
|
|
119
|
+
`Registry de worktrees ocupado: ${registryPath}`,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
sleep(20);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return operation();
|
|
127
|
+
} finally {
|
|
128
|
+
rmSync(lockPath, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function atomicJson(path, value) {
|
|
133
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
134
|
+
const content = `${JSON.stringify(value, null, 2)}\n`;
|
|
135
|
+
if (existsSync(path) && readFileSync(path, 'utf8') === content) return false;
|
|
136
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
137
|
+
writeFileSync(temporary, content, { encoding: 'utf8', flag: 'wx' });
|
|
138
|
+
renameSync(temporary, path);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validateRegistry(value, path) {
|
|
143
|
+
if (
|
|
144
|
+
value?.schemaVersion !== WORKTREE_REGISTRY_SCHEMA
|
|
145
|
+
|| typeof value.repositoryId !== 'string'
|
|
146
|
+
|| !value.repositoryId
|
|
147
|
+
|| typeof value.projectId !== 'string'
|
|
148
|
+
|| !value.projectId
|
|
149
|
+
|| typeof value.vaultPath !== 'string'
|
|
150
|
+
|| !value.vaultPath
|
|
151
|
+
|| !value.entries
|
|
152
|
+
|| typeof value.entries !== 'object'
|
|
153
|
+
|| Array.isArray(value.entries)
|
|
154
|
+
) {
|
|
155
|
+
throw worktreeError(
|
|
156
|
+
'WENDKEEP_WORKTREE_REGISTRY_INVALID',
|
|
157
|
+
`Registry de worktrees incompleto em "${path}".`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function parseRegistry(path) {
|
|
164
|
+
let value;
|
|
165
|
+
try {
|
|
166
|
+
value = JSON.parse(readFileSync(path, 'utf8'));
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw worktreeError(
|
|
169
|
+
'WENDKEEP_WORKTREE_REGISTRY_INVALID',
|
|
170
|
+
`Registry de worktrees inválido em "${path}": ${error.message}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return validateRegistry(value, path);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function readWorktreeRegistry(repository) {
|
|
177
|
+
const path = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
|
|
178
|
+
return existsSync(path) ? { path, registry: parseRegistry(path) } : { path, registry: null };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function resolveWorktreeVaultBinding({ startDir = process.cwd(), projectId } = {}) {
|
|
182
|
+
let repository;
|
|
183
|
+
try {
|
|
184
|
+
repository = discoverWorktreeRepository({ startDir });
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error?.code === 'WENDKEEP_WORKTREE_GIT_FAILED') return null;
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
const { path, registry } = readWorktreeRegistry(repository);
|
|
190
|
+
if (!registry) return null;
|
|
191
|
+
if (registry.projectId !== projectId) {
|
|
192
|
+
throw worktreeError(
|
|
193
|
+
'WENDKEEP_WORKTREE_PROJECT_MISMATCH',
|
|
194
|
+
`Registry "${path}" pertence ao projeto "${registry.projectId}", não "${projectId}".`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
base: resolve(registry.vaultPath),
|
|
199
|
+
projectId: registry.projectId,
|
|
200
|
+
repositoryId: registry.repositoryId,
|
|
201
|
+
registryPath: path,
|
|
202
|
+
projectRoot: repository.mainWorktree,
|
|
203
|
+
repository,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function mutateWorktreeRegistry(repository, mutator) {
|
|
208
|
+
if (!repository?.commonDir || typeof mutator !== 'function') {
|
|
209
|
+
throw worktreeError('WENDKEEP_WORKTREE_REPOSITORY_INVALID', 'Repositório Git inválido.');
|
|
210
|
+
}
|
|
211
|
+
const registryPath = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
|
|
212
|
+
return withWorktreeRegistryLock(registryPath, () => {
|
|
213
|
+
if (!existsSync(registryPath)) {
|
|
214
|
+
throw worktreeError(
|
|
215
|
+
'WENDKEEP_WORKTREE_REGISTRY_MISSING',
|
|
216
|
+
`Registry de worktrees ausente em "${registryPath}".`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
const current = parseRegistry(registryPath);
|
|
220
|
+
const next = validateRegistry(mutator(structuredClone(current)), registryPath);
|
|
221
|
+
atomicJson(registryPath, next);
|
|
222
|
+
return next;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function worktreeIdentity(repositoryId, gitDir) {
|
|
227
|
+
return createHash('sha256')
|
|
228
|
+
.update(`${repositoryId}\n${resolve(gitDir).replaceAll('\\', '/').toLowerCase()}\n`)
|
|
229
|
+
.digest('hex');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function ensureWorktreeMetadata({
|
|
233
|
+
repository,
|
|
234
|
+
projectId,
|
|
235
|
+
vaultPath,
|
|
236
|
+
worktreesRoot = '.worktrees',
|
|
237
|
+
} = {}) {
|
|
238
|
+
if (!repository?.commonDir || !repository?.gitDir) {
|
|
239
|
+
throw worktreeError('WENDKEEP_WORKTREE_REPOSITORY_INVALID', 'Repositório Git inválido.');
|
|
240
|
+
}
|
|
241
|
+
const registryPath = join(repository.commonDir, ...WORKTREE_REGISTRY_REL.split('/'));
|
|
242
|
+
const normalizedVault = resolve(String(vaultPath || ''));
|
|
243
|
+
const registry = withWorktreeRegistryLock(registryPath, () => {
|
|
244
|
+
const current = existsSync(registryPath) ? parseRegistry(registryPath) : null;
|
|
245
|
+
if (current && current.projectId !== projectId) {
|
|
246
|
+
throw worktreeError(
|
|
247
|
+
'WENDKEEP_WORKTREE_PROJECT_MISMATCH',
|
|
248
|
+
`Registry pertence a outro projeto: ${current.projectId}.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
if (current && resolve(current.vaultPath) !== normalizedVault) {
|
|
252
|
+
throw worktreeError(
|
|
253
|
+
'WENDKEEP_WORKTREE_VAULT_MISMATCH',
|
|
254
|
+
'Registry aponta para outro Vault canônico.',
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const next = current || {
|
|
258
|
+
schemaVersion: WORKTREE_REGISTRY_SCHEMA,
|
|
259
|
+
repositoryId: randomUUID(),
|
|
260
|
+
projectId,
|
|
261
|
+
vaultPath: normalizedVault,
|
|
262
|
+
worktreesRoot,
|
|
263
|
+
entries: {},
|
|
264
|
+
};
|
|
265
|
+
atomicJson(registryPath, next);
|
|
266
|
+
return next;
|
|
267
|
+
});
|
|
268
|
+
return {
|
|
269
|
+
registryPath,
|
|
270
|
+
repositoryId: registry.repositoryId,
|
|
271
|
+
currentWorktreeId: worktreeIdentity(registry.repositoryId, repository.gitDir),
|
|
272
|
+
registry,
|
|
273
|
+
};
|
|
274
|
+
}
|