@onebrain-ai/cli 2.0.1 → 2.0.2
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/onebrain +3 -3
- package/package.json +23 -1
- package/src/commands/doctor.test.ts +0 -416
- package/src/commands/doctor.ts +0 -203
- package/src/commands/init.test.ts +0 -318
- package/src/commands/init.ts +0 -477
- package/src/commands/update.test.ts +0 -413
- package/src/commands/update.ts +0 -353
- package/src/index.ts +0 -144
- package/src/internal/__snapshots__/checkpoint.test.ts.snap +0 -12
- package/src/internal/__snapshots__/orphan-scan.test.ts.snap +0 -13
- package/src/internal/__snapshots__/session-init.test.ts.snap +0 -15
- package/src/internal/checkpoint.test.ts +0 -741
- package/src/internal/checkpoint.ts +0 -427
- package/src/internal/migrate.test.ts +0 -301
- package/src/internal/migrate.ts +0 -186
- package/src/internal/orphan-scan.test.ts +0 -271
- package/src/internal/orphan-scan.ts +0 -213
- package/src/internal/qmd-reindex.test.ts +0 -117
- package/src/internal/qmd-reindex.ts +0 -44
- package/src/internal/register-hooks.test.ts +0 -343
- package/src/internal/register-hooks.ts +0 -418
- package/src/internal/session-init.test.ts +0 -318
- package/src/internal/session-init.ts +0 -264
- package/src/internal/vault-sync.test.ts +0 -419
- package/src/internal/vault-sync.ts +0 -764
- package/tests/integration/init.integration.test.ts +0 -304
- package/tests/integration/update.integration.test.ts +0 -306
- package/tsconfig.json +0 -12
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
2
|
-
import { chmodSync } from 'node:fs';
|
|
3
|
-
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
import { runBackfillRecapped } from './migrate.js';
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Helpers
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
async function makeTmpDir(): Promise<string> {
|
|
14
|
-
return mkdtemp(join(tmpdir(), 'onebrain-test-migrate-'));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Make a month directory under logs folder
|
|
19
|
-
*/
|
|
20
|
-
async function makeMonthDir(logsDir: string, year: string, month: string): Promise<string> {
|
|
21
|
-
const dir = join(logsDir, year, month);
|
|
22
|
-
await mkdir(dir, { recursive: true });
|
|
23
|
-
return dir;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Create a session log file with optional frontmatter
|
|
28
|
-
*/
|
|
29
|
-
async function writeSessionLog(
|
|
30
|
-
dir: string,
|
|
31
|
-
filename: string,
|
|
32
|
-
frontmatter?: Record<string, unknown>,
|
|
33
|
-
): Promise<void> {
|
|
34
|
-
let content = '';
|
|
35
|
-
if (frontmatter) {
|
|
36
|
-
content += '---\n';
|
|
37
|
-
for (const [key, value] of Object.entries(frontmatter)) {
|
|
38
|
-
if (typeof value === 'string') {
|
|
39
|
-
content += `${key}: ${value}\n`;
|
|
40
|
-
} else if (typeof value === 'boolean') {
|
|
41
|
-
content += `${key}: ${value}\n`;
|
|
42
|
-
} else {
|
|
43
|
-
content += `${key}: ${JSON.stringify(value)}\n`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
content += '---\n';
|
|
47
|
-
}
|
|
48
|
-
content += '\n## Session\n\nTest content\n';
|
|
49
|
-
|
|
50
|
-
await writeFile(join(dir, filename), content);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Tests
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
describe('runBackfillRecapped', () => {
|
|
58
|
-
let tmpDir: string;
|
|
59
|
-
let logsDir: string;
|
|
60
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
61
|
-
|
|
62
|
-
beforeEach(async () => {
|
|
63
|
-
tmpDir = await makeTmpDir();
|
|
64
|
-
logsDir = join(tmpDir, '07-logs');
|
|
65
|
-
await mkdir(logsDir, { recursive: true });
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(async () => {
|
|
69
|
-
await rm(tmpDir, { recursive: true, force: true });
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('returns backfilled: 0, skipped: 0 for empty logs dir', async () => {
|
|
73
|
-
const result = await runBackfillRecapped(logsDir);
|
|
74
|
-
expect(result).toEqual({ backfilled: 0, skipped: 0 });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('returns backfilled: 0, skipped: 0 when logs dir does not exist', async () => {
|
|
78
|
-
const nonexistent = join(tmpDir, 'nonexistent');
|
|
79
|
-
const result = await runBackfillRecapped(nonexistent);
|
|
80
|
-
expect(result).toEqual({ backfilled: 0, skipped: 0 });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('skips checkpoint files (filename contains -checkpoint-)', async () => {
|
|
84
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
85
|
-
await writeSessionLog(monthDir, '2026-04-20-abc12345-checkpoint-01.md', {
|
|
86
|
-
tags: 'checkpoint',
|
|
87
|
-
checkpoint: '01',
|
|
88
|
-
recapped: false,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const result = await runBackfillRecapped(logsDir);
|
|
92
|
-
expect(result).toEqual({ backfilled: 0, skipped: 0 });
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('skips files that already have recapped field', async () => {
|
|
96
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
97
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
98
|
-
tags: 'session-log',
|
|
99
|
-
recapped: '2026-04-20',
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
const result = await runBackfillRecapped(logsDir);
|
|
103
|
-
expect(result).toEqual({ backfilled: 0, skipped: 0 });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('skips session log with recapped: false (field present, falsy value)', async () => {
|
|
107
|
-
// recapped: false means "field exists" → !== undefined → skip
|
|
108
|
-
// A falsy check (!recapped) would incorrectly backfill this file
|
|
109
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
110
|
-
await writeSessionLog(monthDir, '2026-04-21-session-01.md', {
|
|
111
|
-
tags: 'session-log',
|
|
112
|
-
recapped: false,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const result = await runBackfillRecapped(logsDir);
|
|
116
|
-
expect(result).toEqual({ backfilled: 0, skipped: 0 });
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('adds recapped field to session log missing it', async () => {
|
|
120
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
121
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
122
|
-
tags: 'session-log',
|
|
123
|
-
date: '2026-04-20',
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const result = await runBackfillRecapped(logsDir);
|
|
127
|
-
expect(result).toEqual({ backfilled: 1, skipped: 0 });
|
|
128
|
-
|
|
129
|
-
// Verify file was updated
|
|
130
|
-
const content = await Bun.file(join(monthDir, '2026-04-20-session-01.md')).text();
|
|
131
|
-
expect(content).toContain(`recapped: ${today}`);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('skips files with malformed frontmatter', async () => {
|
|
135
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
136
|
-
// Missing closing ---
|
|
137
|
-
await writeFile(
|
|
138
|
-
join(monthDir, '2026-04-20-session-01.md'),
|
|
139
|
-
'---\ntags: session-log\ndate: 2026-04-20\n\n## Session\nContent\n',
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
const result = await runBackfillRecapped(logsDir);
|
|
143
|
-
expect(result.skipped).toBeGreaterThan(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it('processes multiple files in multiple months', async () => {
|
|
147
|
-
const monthDir1 = await makeMonthDir(logsDir, '2026', '04');
|
|
148
|
-
const monthDir2 = await makeMonthDir(logsDir, '2026', '03');
|
|
149
|
-
|
|
150
|
-
// File in April without recapped
|
|
151
|
-
await writeSessionLog(monthDir1, '2026-04-20-session-01.md', {
|
|
152
|
-
tags: 'session-log',
|
|
153
|
-
date: '2026-04-20',
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// File in March without recapped
|
|
157
|
-
await writeSessionLog(monthDir2, '2026-03-15-session-01.md', {
|
|
158
|
-
tags: 'session-log',
|
|
159
|
-
date: '2026-03-15',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
// File that already has recapped
|
|
163
|
-
await writeSessionLog(monthDir1, '2026-04-19-session-01.md', {
|
|
164
|
-
tags: 'session-log',
|
|
165
|
-
date: '2026-04-19',
|
|
166
|
-
recapped: '2026-04-19',
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
const result = await runBackfillRecapped(logsDir);
|
|
170
|
-
expect(result).toEqual({ backfilled: 2, skipped: 0 });
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('skips files without frontmatter', async () => {
|
|
174
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
175
|
-
// File with no frontmatter
|
|
176
|
-
await writeFile(join(monthDir, '2026-04-20-session-01.md'), '## Session\n\nContent\n');
|
|
177
|
-
|
|
178
|
-
const result = await runBackfillRecapped(logsDir);
|
|
179
|
-
expect(result).toEqual({ backfilled: 0, skipped: 1 });
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('does not treat ---something as a closing frontmatter delimiter', async () => {
|
|
183
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
184
|
-
// Body contains a line starting with ---foo which must NOT close the frontmatter.
|
|
185
|
-
// The real closing --- (bare, followed by newline) appears later.
|
|
186
|
-
const content = [
|
|
187
|
-
'---',
|
|
188
|
-
'tags: session-log',
|
|
189
|
-
'date: 2026-04-20',
|
|
190
|
-
'---',
|
|
191
|
-
'',
|
|
192
|
-
'## Session',
|
|
193
|
-
'',
|
|
194
|
-
'---some-separator',
|
|
195
|
-
'',
|
|
196
|
-
'Content below separator.',
|
|
197
|
-
].join('\n');
|
|
198
|
-
await writeFile(join(monthDir, '2026-04-20-session-01.md'), content);
|
|
199
|
-
|
|
200
|
-
const result = await runBackfillRecapped(logsDir);
|
|
201
|
-
// File has valid frontmatter (no recapped), should be backfilled
|
|
202
|
-
expect(result).toEqual({ backfilled: 1, skipped: 0 });
|
|
203
|
-
|
|
204
|
-
// Verify frontmatter was updated correctly without corrupting the body
|
|
205
|
-
const updated = await Bun.file(join(monthDir, '2026-04-20-session-01.md')).text();
|
|
206
|
-
expect(updated).toContain(`recapped: ${today}`);
|
|
207
|
-
expect(updated).toContain('---some-separator');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('preserves existing frontmatter fields when adding recapped', async () => {
|
|
211
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
212
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
213
|
-
tags: 'session-log',
|
|
214
|
-
date: '2026-04-20',
|
|
215
|
-
foo: 'bar',
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const result = await runBackfillRecapped(logsDir);
|
|
219
|
-
expect(result).toEqual({ backfilled: 1, skipped: 0 });
|
|
220
|
-
|
|
221
|
-
// Verify all fields are preserved
|
|
222
|
-
const content = await Bun.file(join(monthDir, '2026-04-20-session-01.md')).text();
|
|
223
|
-
expect(content).toContain('tags: session-log');
|
|
224
|
-
expect(content).toContain('date: 2026-04-20');
|
|
225
|
-
expect(content).toContain('foo: bar');
|
|
226
|
-
expect(content).toContain(`recapped: ${today}`);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('writeFile failure (EACCES via chmodSync): skipped === 1, backfilled === 1 (other file writable)', async () => {
|
|
230
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
231
|
-
|
|
232
|
-
// File 1: readable, writable — will be backfilled
|
|
233
|
-
const _file1 = join(monthDir, '2026-04-20-session-01.md');
|
|
234
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
235
|
-
tags: 'session-log',
|
|
236
|
-
date: '2026-04-20',
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// File 2: readable, but write-protected — will cause EACCES on writeFile
|
|
240
|
-
const file2 = join(monthDir, '2026-04-21-session-01.md');
|
|
241
|
-
await writeSessionLog(monthDir, '2026-04-21-session-01.md', {
|
|
242
|
-
tags: 'session-log',
|
|
243
|
-
date: '2026-04-21',
|
|
244
|
-
});
|
|
245
|
-
chmodSync(file2, 0o444);
|
|
246
|
-
|
|
247
|
-
try {
|
|
248
|
-
const result = await runBackfillRecapped(logsDir);
|
|
249
|
-
// One file successfully backfilled, one failed due to EACCES
|
|
250
|
-
expect(result.backfilled).toBe(1);
|
|
251
|
-
expect(result.skipped).toBe(1);
|
|
252
|
-
} finally {
|
|
253
|
-
// Restore permissions so cleanup works
|
|
254
|
-
chmodSync(file2, 0o644);
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('all files read-only: backfilled === 0, skipped === 2', async () => {
|
|
259
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
260
|
-
|
|
261
|
-
const file1 = join(monthDir, '2026-04-20-session-01.md');
|
|
262
|
-
const file2 = join(monthDir, '2026-04-21-session-01.md');
|
|
263
|
-
|
|
264
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
265
|
-
tags: 'session-log',
|
|
266
|
-
date: '2026-04-20',
|
|
267
|
-
});
|
|
268
|
-
await writeSessionLog(monthDir, '2026-04-21-session-01.md', {
|
|
269
|
-
tags: 'session-log',
|
|
270
|
-
date: '2026-04-21',
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
chmodSync(file1, 0o444);
|
|
274
|
-
chmodSync(file2, 0o444);
|
|
275
|
-
|
|
276
|
-
try {
|
|
277
|
-
const result = await runBackfillRecapped(logsDir);
|
|
278
|
-
expect(result.backfilled).toBe(0);
|
|
279
|
-
expect(result.skipped).toBe(2);
|
|
280
|
-
} finally {
|
|
281
|
-
chmodSync(file1, 0o644);
|
|
282
|
-
chmodSync(file2, 0o644);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('handles idempotent re-runs: only first run backfills', async () => {
|
|
287
|
-
const monthDir = await makeMonthDir(logsDir, '2026', '04');
|
|
288
|
-
await writeSessionLog(monthDir, '2026-04-20-session-01.md', {
|
|
289
|
-
tags: 'session-log',
|
|
290
|
-
date: '2026-04-20',
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// First run
|
|
294
|
-
const result1 = await runBackfillRecapped(logsDir);
|
|
295
|
-
expect(result1).toEqual({ backfilled: 1, skipped: 0 });
|
|
296
|
-
|
|
297
|
-
// Second run — should skip since recapped now exists
|
|
298
|
-
const result2 = await runBackfillRecapped(logsDir);
|
|
299
|
-
expect(result2).toEqual({ backfilled: 0, skipped: 0 });
|
|
300
|
-
});
|
|
301
|
-
});
|
package/src/internal/migrate.ts
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* migrate — internal command
|
|
3
|
-
*
|
|
4
|
-
* Runs one-time migration scripts to update vault state.
|
|
5
|
-
* Currently supports `backfill-recapped` migration.
|
|
6
|
-
*
|
|
7
|
-
* Output: plain text summary (not JSON)
|
|
8
|
-
* Exit code: always 0 (internal pattern)
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
12
|
-
import { join } from 'node:path';
|
|
13
|
-
import { loadVaultConfig } from '@onebrain/core';
|
|
14
|
-
import { parse, stringify } from 'yaml';
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Types
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export type MigrateResult = {
|
|
21
|
-
backfilled: number;
|
|
22
|
-
skipped: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Frontmatter helpers
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Extract YAML frontmatter from markdown text.
|
|
31
|
-
* Returns { frontmatter object, remainingText } or null if no valid frontmatter.
|
|
32
|
-
*/
|
|
33
|
-
function parseFrontmatterWithRest(rawText: string): {
|
|
34
|
-
frontmatter: Record<string, unknown>;
|
|
35
|
-
rest: string;
|
|
36
|
-
} | null {
|
|
37
|
-
const text = rawText.replace(/\r\n/g, '\n');
|
|
38
|
-
if (!text.startsWith('---')) return null;
|
|
39
|
-
|
|
40
|
-
// Require closing --- to be followed by newline or end-of-string (bare --- line only).
|
|
41
|
-
// Rejects lines like ---foo or ---some-separator as false closers.
|
|
42
|
-
const endMatch = /\n---(\n|$)/.exec(text.slice(3));
|
|
43
|
-
if (!endMatch) return null;
|
|
44
|
-
const endIdx = 3 + endMatch.index;
|
|
45
|
-
const rest = text.slice(endIdx + endMatch[0].length);
|
|
46
|
-
|
|
47
|
-
const fmText = text.slice(3, endIdx).trim();
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const parsed = parse(fmText);
|
|
51
|
-
if (parsed && typeof parsed === 'object') {
|
|
52
|
-
return {
|
|
53
|
-
frontmatter: parsed as Record<string, unknown>,
|
|
54
|
-
rest,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// File listing helper
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
async function listMdFiles(dir: string): Promise<string[]> {
|
|
68
|
-
try {
|
|
69
|
-
const entries = await readdir(dir);
|
|
70
|
-
return entries.filter((e) => e.endsWith('.md'));
|
|
71
|
-
} catch {
|
|
72
|
-
return [];
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
// runBackfillRecapped (testable core)
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Core logic for backfill-recapped migration.
|
|
82
|
-
* Scans session logs and adds `recapped: <today-date>` to frontmatter where missing.
|
|
83
|
-
*
|
|
84
|
-
* @param logsFolder - absolute path to logs folder
|
|
85
|
-
* @returns MigrateResult with backfilled and skipped counts
|
|
86
|
-
*/
|
|
87
|
-
export async function runBackfillRecapped(logsFolder: string): Promise<MigrateResult> {
|
|
88
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
89
|
-
let backfilled = 0;
|
|
90
|
-
let skipped = 0;
|
|
91
|
-
|
|
92
|
-
// List all year directories
|
|
93
|
-
let yearDirs: string[] = [];
|
|
94
|
-
try {
|
|
95
|
-
yearDirs = await readdir(logsFolder);
|
|
96
|
-
} catch {
|
|
97
|
-
// Logs folder doesn't exist; return empty result
|
|
98
|
-
return { backfilled: 0, skipped: 0 };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
for (const yearDir of yearDirs) {
|
|
102
|
-
const yearPath = join(logsFolder, yearDir);
|
|
103
|
-
|
|
104
|
-
// List all month directories under year
|
|
105
|
-
let monthDirs: string[] = [];
|
|
106
|
-
try {
|
|
107
|
-
monthDirs = await readdir(yearPath);
|
|
108
|
-
} catch {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
for (const monthDir of monthDirs) {
|
|
113
|
-
const monthPath = join(yearPath, monthDir);
|
|
114
|
-
const files = await listMdFiles(monthPath);
|
|
115
|
-
|
|
116
|
-
for (const fname of files) {
|
|
117
|
-
const fpath = join(monthPath, fname);
|
|
118
|
-
|
|
119
|
-
// Skip checkpoint files
|
|
120
|
-
if (fname.includes('-checkpoint-')) {
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const content = await readFile(fpath, 'utf8');
|
|
126
|
-
const parsed = parseFrontmatterWithRest(content);
|
|
127
|
-
|
|
128
|
-
if (!parsed) {
|
|
129
|
-
// No frontmatter or malformed
|
|
130
|
-
process.stderr.write(`migrate: ${fname} — malformed frontmatter\n`);
|
|
131
|
-
skipped++;
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const { frontmatter, rest } = parsed;
|
|
136
|
-
|
|
137
|
-
// Skip if already has recapped
|
|
138
|
-
if (frontmatter.recapped !== undefined) {
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Add recapped field
|
|
143
|
-
frontmatter.recapped = today;
|
|
144
|
-
|
|
145
|
-
// Rebuild file with updated frontmatter
|
|
146
|
-
const updatedFm = stringify(frontmatter);
|
|
147
|
-
const updatedContent = `---\n${updatedFm}---\n${rest}`;
|
|
148
|
-
|
|
149
|
-
await writeFile(fpath, updatedContent, 'utf8');
|
|
150
|
-
backfilled++;
|
|
151
|
-
} catch (error) {
|
|
152
|
-
process.stderr.write(`migrate: error processing ${fname}: ${error}\n`);
|
|
153
|
-
skipped++;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { backfilled, skipped };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ---------------------------------------------------------------------------
|
|
163
|
-
// CLI entry point
|
|
164
|
-
// ---------------------------------------------------------------------------
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Run migrate as a CLI command.
|
|
168
|
-
* Currently supports 'backfill-recapped' migration.
|
|
169
|
-
* Always exits 0 (internal pattern).
|
|
170
|
-
*/
|
|
171
|
-
export async function migrateCommand(migrationName: string): Promise<void> {
|
|
172
|
-
try {
|
|
173
|
-
const vaultRoot = process.cwd();
|
|
174
|
-
const config = await loadVaultConfig(vaultRoot);
|
|
175
|
-
const logsFolder = join(vaultRoot, config.folders.logs);
|
|
176
|
-
|
|
177
|
-
if (migrationName === 'backfill-recapped') {
|
|
178
|
-
const result = await runBackfillRecapped(logsFolder);
|
|
179
|
-
process.stdout.write(`backfilled: ${result.backfilled} files, skipped: ${result.skipped}\n`);
|
|
180
|
-
} else {
|
|
181
|
-
process.stderr.write(`migrate: unknown migration '${migrationName}'\n`);
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
process.stderr.write(`migrate: ${error}\n`);
|
|
185
|
-
}
|
|
186
|
-
}
|