@andespindola/brainlink 0.1.0-beta.7 → 0.1.0-beta.8
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/README.md
CHANGED
|
@@ -561,9 +561,11 @@ Every command works with either `brainlink` or `blink`.
|
|
|
561
561
|
```bash
|
|
562
562
|
blink init
|
|
563
563
|
blink init ./vault
|
|
564
|
+
blink init ./team-vault --migrate-from ~/.brainlink/vault
|
|
564
565
|
```
|
|
565
566
|
|
|
566
567
|
Initializes vault metadata. Without an argument, Brainlink initializes the default vault at `$HOME/.brainlink/vault`.
|
|
568
|
+
When initializing an empty custom vault, existing Markdown content from the default vault is copied into it and reindexed so context is not left behind. Use `--no-migrate-existing` to start with an empty custom vault, or `--migrate-from <vault>` to copy from a specific source. Existing target files are never overwritten; conflicting source files are preserved with a `.conflict-<timestamp>` suffix.
|
|
567
569
|
|
|
568
570
|
### `add`
|
|
569
571
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, extname, isAbsolute, join, relative } from 'node:path';
|
|
3
|
+
import { ensureVault, listVaultFiles, resolveVaultPath } from '../infrastructure/file-system-vault.js';
|
|
4
|
+
const directoryMode = 0o700;
|
|
5
|
+
const fileMode = 0o600;
|
|
6
|
+
const timestamp = () => new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
|
|
7
|
+
const isPathInside = (parent, child) => {
|
|
8
|
+
const path = relative(parent, child);
|
|
9
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
10
|
+
};
|
|
11
|
+
const conflictPath = (targetPath) => {
|
|
12
|
+
const extension = extname(targetPath);
|
|
13
|
+
const base = extension ? targetPath.slice(0, -extension.length) : targetPath;
|
|
14
|
+
return `${base}.conflict-${timestamp()}${extension}`;
|
|
15
|
+
};
|
|
16
|
+
const writePreservedFile = async (absolutePath, content) => {
|
|
17
|
+
await mkdir(dirname(absolutePath), { recursive: true, mode: directoryMode });
|
|
18
|
+
await writeFile(absolutePath, content, { mode: fileMode });
|
|
19
|
+
await chmod(absolutePath, fileMode);
|
|
20
|
+
};
|
|
21
|
+
export const migrateVaultContent = async (sourceVault, targetVault) => {
|
|
22
|
+
const source = await ensureVault(sourceVault);
|
|
23
|
+
const target = await ensureVault(targetVault);
|
|
24
|
+
if (source === target) {
|
|
25
|
+
return { source, target, copied: 0, unchanged: 0, conflicted: 0 };
|
|
26
|
+
}
|
|
27
|
+
const sourceFiles = await listVaultFiles(source);
|
|
28
|
+
const migrated = await sourceFiles.reduce(async (statePromise, sourceFile) => {
|
|
29
|
+
const state = await statePromise;
|
|
30
|
+
const targetFile = join(target, relative(source, sourceFile));
|
|
31
|
+
if (!isPathInside(target, targetFile)) {
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
const sourceContent = await readFile(sourceFile);
|
|
35
|
+
try {
|
|
36
|
+
const targetContent = await readFile(targetFile);
|
|
37
|
+
if (sourceContent.equals(targetContent)) {
|
|
38
|
+
return { ...state, unchanged: state.unchanged + 1 };
|
|
39
|
+
}
|
|
40
|
+
await writePreservedFile(conflictPath(targetFile), sourceContent);
|
|
41
|
+
return { ...state, conflicted: state.conflicted + 1 };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
await writePreservedFile(targetFile, sourceContent);
|
|
48
|
+
return { ...state, copied: state.copied + 1 };
|
|
49
|
+
}
|
|
50
|
+
}, Promise.resolve({ source, target, copied: 0, unchanged: 0, conflicted: 0 }));
|
|
51
|
+
return migrated;
|
|
52
|
+
};
|
|
53
|
+
export const shouldMigrateDefaultVault = async (sourceVault, targetVault) => {
|
|
54
|
+
const source = resolveVaultPath(sourceVault);
|
|
55
|
+
const target = resolveVaultPath(targetVault);
|
|
56
|
+
if (source === target) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
const [sourceFiles, targetFiles] = await Promise.all([listVaultFiles(source), listVaultFiles(target)]);
|
|
60
|
+
return sourceFiles.length > 0 && targetFiles.length === 0;
|
|
61
|
+
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { addNote } from '../../application/add-note.js';
|
|
3
3
|
import { indexVault } from '../../application/index-vault.js';
|
|
4
|
+
import { migrateVaultContent, shouldMigrateDefaultVault } from '../../application/migrate-vault.js';
|
|
4
5
|
import { startServer } from '../../application/start-server.js';
|
|
5
6
|
import { startVaultWatcher } from '../../application/watch-vault.js';
|
|
6
7
|
import { doctorVault } from '../../application/analyze-vault.js';
|
|
8
|
+
import { defaultBrainlinkConfig } from '../../infrastructure/config.js';
|
|
7
9
|
import { loadBrainlinkConfig } from '../../infrastructure/config.js';
|
|
8
10
|
import { assertVaultAllowed, ensureVault } from '../../infrastructure/file-system-vault.js';
|
|
9
11
|
import { parsePositiveInteger, print, resolveOptions } from '../runtime.js';
|
|
@@ -20,12 +22,26 @@ export const registerWriteCommands = (program) => {
|
|
|
20
22
|
program
|
|
21
23
|
.command('init')
|
|
22
24
|
.argument('[vault]', 'vault directory')
|
|
25
|
+
.option('--migrate-from <vault>', 'copy existing vault content into the initialized vault')
|
|
26
|
+
.option('--no-migrate-existing', 'skip automatic migration from the default Brainlink vault into an empty custom vault')
|
|
23
27
|
.option('--json', 'print machine-readable JSON')
|
|
24
28
|
.description('initialize a Brainlink vault')
|
|
25
29
|
.action(async (vault, options) => {
|
|
26
30
|
const config = await loadBrainlinkConfig();
|
|
27
|
-
const
|
|
28
|
-
|
|
31
|
+
const targetVault = assertVaultAllowed(vault ?? config.vault, config.allowedVaults);
|
|
32
|
+
const path = await ensureVault(targetVault);
|
|
33
|
+
const explicitSource = options.migrateFrom ? assertVaultAllowed(options.migrateFrom, config.allowedVaults) : undefined;
|
|
34
|
+
const shouldAutoMigrate = explicitSource === undefined &&
|
|
35
|
+
options.migrateExisting !== false &&
|
|
36
|
+
(await shouldMigrateDefaultVault(defaultBrainlinkConfig.vault, targetVault));
|
|
37
|
+
const migration = explicitSource || shouldAutoMigrate ? await migrateVaultContent(explicitSource ?? defaultBrainlinkConfig.vault, targetVault) : undefined;
|
|
38
|
+
const index = migration && migration.copied + migration.conflicted > 0 ? await indexVault(targetVault) : undefined;
|
|
39
|
+
print(options.json, { path, ...(migration ? { migration } : {}), ...(index ? { index } : {}) }, () => {
|
|
40
|
+
const migrated = migration
|
|
41
|
+
? ` Migrated ${migration.copied} files, preserved ${migration.conflicted} conflicts and kept ${migration.unchanged} unchanged files.`
|
|
42
|
+
: '';
|
|
43
|
+
return `Initialized Brainlink vault at ${path}.${migrated}`;
|
|
44
|
+
});
|
|
29
45
|
});
|
|
30
46
|
program
|
|
31
47
|
.command('add')
|
|
@@ -16,6 +16,17 @@ const walkMarkdownFiles = async (directory) => {
|
|
|
16
16
|
}));
|
|
17
17
|
return nested.flat();
|
|
18
18
|
};
|
|
19
|
+
const walkVaultFiles = async (directory) => {
|
|
20
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
21
|
+
const nested = await Promise.all(entries.map(async (entry) => {
|
|
22
|
+
const absolutePath = join(directory, entry.name);
|
|
23
|
+
if (entry.isDirectory()) {
|
|
24
|
+
return excludedDirectories.has(entry.name) ? [] : walkVaultFiles(absolutePath);
|
|
25
|
+
}
|
|
26
|
+
return entry.isFile() ? [absolutePath] : [];
|
|
27
|
+
}));
|
|
28
|
+
return nested.flat();
|
|
29
|
+
};
|
|
19
30
|
export const resolveVaultPath = (vaultPath) => isBucketVaultUri(vaultPath) ? getBucketVaultCachePath(vaultPath) : resolvePath(vaultPath);
|
|
20
31
|
export const isBucketVaultPath = (vaultPath) => isBucketVaultUri(vaultPath);
|
|
21
32
|
const isPathInside = (parent, child) => {
|
|
@@ -65,6 +76,10 @@ export const readMarkdownFiles = async (vaultPath) => {
|
|
|
65
76
|
};
|
|
66
77
|
}));
|
|
67
78
|
};
|
|
79
|
+
export const listVaultFiles = async (vaultPath) => {
|
|
80
|
+
const absoluteVaultPath = await ensureVault(vaultPath);
|
|
81
|
+
return walkVaultFiles(absoluteVaultPath);
|
|
82
|
+
};
|
|
68
83
|
export const writeMarkdownFile = async (vaultPath, filename, content) => {
|
|
69
84
|
if (isBucketVaultUri(vaultPath)) {
|
|
70
85
|
return writeBucketMarkdownFile(vaultPath, filename, content);
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -341,7 +341,7 @@ $HOME/.brainlink/vault/
|
|
|
341
341
|
.brainlink/
|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
`blink init ./vault` creates a custom vault instead.
|
|
344
|
+
`blink init ./vault` creates a custom vault instead. If the custom vault is empty and the default `$HOME/.brainlink/vault` already has Markdown memory, Brainlink copies that content into the custom vault and reindexes it. Use `blink init ./vault --no-migrate-existing` to intentionally start empty, or `blink init ./vault --migrate-from <old-vault>` to migrate from a specific previous vault. Existing target files are not overwritten; conflicting source files are preserved with a `.conflict-<timestamp>` suffix.
|
|
345
345
|
|
|
346
346
|
### Add A Note
|
|
347
347
|
|
package/package.json
CHANGED