total-recall-brain 3.40.0 → 3.40.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/package.json +1 -1
- package/src/cli/backup.mjs +71 -3
package/package.json
CHANGED
package/src/cli/backup.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
20
21
|
import fs from 'node:fs';
|
|
21
22
|
import os from 'node:os';
|
|
22
23
|
import path from 'node:path';
|
|
@@ -64,8 +65,22 @@ function parseArgs(args) {
|
|
|
64
65
|
return opts;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* The backup repo's git directory lives OUTSIDE the project tree, under
|
|
70
|
+
* ~/.agent/brain-backups. A `.git` inside `<project>/.agent/skills/total-recall`
|
|
71
|
+
* makes the project repo see an embedded repository: `git add -A` records it as a
|
|
72
|
+
* gitlink (mode 160000) with no .gitmodules, which cannot be cloned back.
|
|
73
|
+
*/
|
|
74
|
+
export function brainGitDir(skillDir) {
|
|
75
|
+
const abs = path.resolve(skillDir);
|
|
76
|
+
const owner = path.basename(path.resolve(abs, '..', '..', '..')) || 'brain';
|
|
77
|
+
const tag = crypto.createHash('sha1').update(abs).digest('hex').slice(0, 8);
|
|
78
|
+
return path.join(os.homedir(), '.agent', 'brain-backups', `${owner}-${tag}.git`);
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
function git(cwd, ...gitArgs) {
|
|
68
|
-
const
|
|
82
|
+
const env = { ...process.env, GIT_DIR: brainGitDir(cwd), GIT_WORK_TREE: path.resolve(cwd) };
|
|
83
|
+
const result = spawnSync('git', gitArgs, { cwd, env, stdio: 'pipe' });
|
|
69
84
|
return {
|
|
70
85
|
ok: result.status === 0,
|
|
71
86
|
stdout: result.stdout?.toString().trim() || '',
|
|
@@ -73,6 +88,56 @@ function git(cwd, ...gitArgs) {
|
|
|
73
88
|
};
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Move a legacy in-tree `<brain>/.git` to the external git dir, and keep the
|
|
93
|
+
* enclosing project repo from absorbing the brain directory. Idempotent.
|
|
94
|
+
*/
|
|
95
|
+
export function adoptExternalGitDir(skillDir) {
|
|
96
|
+
const inTree = path.join(skillDir, '.git');
|
|
97
|
+
const external = brainGitDir(skillDir);
|
|
98
|
+
let moved = false;
|
|
99
|
+
if (fs.existsSync(inTree) && fs.statSync(inTree).isDirectory() && !fs.existsSync(external)) {
|
|
100
|
+
fs.mkdirSync(path.dirname(external), { recursive: true });
|
|
101
|
+
try {
|
|
102
|
+
fs.renameSync(inTree, external);
|
|
103
|
+
} catch {
|
|
104
|
+
fs.cpSync(inTree, external, { recursive: true });
|
|
105
|
+
fs.rmSync(inTree, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
spawnSync('git', ['--git-dir', external, 'config', 'core.worktree', path.resolve(skillDir)], { stdio: 'pipe' });
|
|
108
|
+
moved = true;
|
|
109
|
+
}
|
|
110
|
+
if (fs.existsSync(external) && !fs.existsSync(inTree)) excludeFromParentRepo(skillDir);
|
|
111
|
+
return moved;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Add the brain dir to the enclosing repo's local exclude unless that repo tracks it on purpose. */
|
|
115
|
+
function excludeFromParentRepo(skillDir) {
|
|
116
|
+
try {
|
|
117
|
+
const top = spawnSync('git', ['-C', skillDir, 'rev-parse', '--show-toplevel'], { stdio: 'pipe' });
|
|
118
|
+
if (top.status !== 0) return;
|
|
119
|
+
const root = top.stdout.toString().trim();
|
|
120
|
+
const rel = path.relative(fs.realpathSync(root), fs.realpathSync(skillDir)).split(path.sep).join('/');
|
|
121
|
+
if (!rel || rel.startsWith('..')) return;
|
|
122
|
+
const tracked = spawnSync('git', ['-C', root, 'ls-files', '-s', '--', rel], { stdio: 'pipe' }).stdout.toString().trim();
|
|
123
|
+
if (/^160000 /.test(tracked) && !tracked.includes('\n')) {
|
|
124
|
+
// A stale gitlink (embedded repo with no .gitmodules) cannot be cloned back: unstage it.
|
|
125
|
+
spawnSync('git', ['-C', root, 'rm', '--cached', '-f', '-q', '--', rel], { stdio: 'pipe' });
|
|
126
|
+
} else if (tracked) {
|
|
127
|
+
return; // the project repo versions its brain files itself
|
|
128
|
+
}
|
|
129
|
+
const excludePath = path.join(root, '.git', 'info', 'exclude');
|
|
130
|
+
try {
|
|
131
|
+
const entry = `/${rel}/`;
|
|
132
|
+
const current = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, 'utf8') : '';
|
|
133
|
+
if (!current.split('\n').includes(entry)) {
|
|
134
|
+
fs.mkdirSync(path.dirname(excludePath), { recursive: true });
|
|
135
|
+
fs.appendFileSync(excludePath, `${current && !current.endsWith('\n') ? '\n' : ''}${entry}\n`);
|
|
136
|
+
}
|
|
137
|
+
} catch { /* parent exclude is best effort */ }
|
|
138
|
+
} catch { /* best effort: never fail a backup over the parent's exclude file */ }
|
|
139
|
+
}
|
|
140
|
+
|
|
76
141
|
function defaultOutput(encrypt) {
|
|
77
142
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
78
143
|
const suffix = encrypt ? '.tar.gz.gpg' : '.tar.gz';
|
|
@@ -169,8 +234,10 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
169
234
|
// rule are corrected on their next backup.
|
|
170
235
|
ensureBrainGitignore(SKILL_DIR);
|
|
171
236
|
|
|
172
|
-
// Initialise the skill folder as a git repo if needed
|
|
173
|
-
|
|
237
|
+
// Initialise the skill folder as a git repo if needed. The git dir lives outside
|
|
238
|
+
// the project tree; a legacy in-tree .git is moved there first.
|
|
239
|
+
adoptExternalGitDir(SKILL_DIR);
|
|
240
|
+
const gitDir = brainGitDir(SKILL_DIR);
|
|
174
241
|
if (!fs.existsSync(gitDir)) {
|
|
175
242
|
console.error(` 🔧 Initialising git repo in ${SKILL_DIR}`);
|
|
176
243
|
const init = git(SKILL_DIR, 'init', '-b', 'main');
|
|
@@ -178,6 +245,7 @@ async function pushGitBackup(remote, layer = 'auto') {
|
|
|
178
245
|
git(SKILL_DIR, 'init');
|
|
179
246
|
git(SKILL_DIR, 'checkout', '-b', 'main');
|
|
180
247
|
}
|
|
248
|
+
excludeFromParentRepo(SKILL_DIR);
|
|
181
249
|
}
|
|
182
250
|
|
|
183
251
|
// Resolve the remote: if the argument is an existing remote name, use its URL.
|