dig-burrow 1.2.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/.claude/burrow/VERSION +1 -0
- package/.claude/burrow/burrow-tools.cjs +490 -0
- package/.claude/burrow/lib/core.cjs +26 -0
- package/.claude/burrow/lib/init.cjs +95 -0
- package/.claude/burrow/lib/installer.cjs +389 -0
- package/.claude/burrow/lib/mongoose.cjs +461 -0
- package/.claude/burrow/lib/render.cjs +330 -0
- package/.claude/burrow/lib/version.cjs +137 -0
- package/.claude/burrow/lib/warren.cjs +168 -0
- package/.claude/burrow/workflows/burrow.md +184 -0
- package/.claude/commands/burrow/add.md +12 -0
- package/.claude/commands/burrow/archive.md +12 -0
- package/.claude/commands/burrow/dump.md +12 -0
- package/.claude/commands/burrow/edit.md +12 -0
- package/.claude/commands/burrow/help.md +29 -0
- package/.claude/commands/burrow/move.md +12 -0
- package/.claude/commands/burrow/read.md +12 -0
- package/.claude/commands/burrow/remove.md +12 -0
- package/.claude/commands/burrow/unarchive.md +12 -0
- package/.claude/commands/burrow/update.md +15 -0
- package/.claude/commands/burrow.md +16 -0
- package/LICENSE +21 -0
- package/README.md +285 -0
- package/install.cjs +430 -0
- package/package.json +31 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.2.0
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { parseArgs } = require('node:util');
|
|
5
|
+
|
|
6
|
+
const core = require('./lib/core.cjs');
|
|
7
|
+
const storage = require('./lib/warren.cjs');
|
|
8
|
+
const tree = require('./lib/mongoose.cjs');
|
|
9
|
+
const render = require('./lib/render.cjs');
|
|
10
|
+
const { init } = require('./lib/init.cjs');
|
|
11
|
+
const version = require('./lib/version.cjs');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve terminal width from --width flag or process.stdout.columns.
|
|
15
|
+
* @param {object} values - Parsed CLI values (may have values.width)
|
|
16
|
+
* @returns {number} Terminal width to use for rendering
|
|
17
|
+
*/
|
|
18
|
+
function resolveTermWidth(values) {
|
|
19
|
+
if (values.width !== undefined) {
|
|
20
|
+
const n = parseInt(values.width, 10);
|
|
21
|
+
if (!isNaN(n) && n > 0) return n;
|
|
22
|
+
}
|
|
23
|
+
return process.stdout.columns || 80;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Handle error output: human-readable rendered error.
|
|
28
|
+
* @param {string} message - Error description
|
|
29
|
+
*/
|
|
30
|
+
function handleError(message) {
|
|
31
|
+
process.stdout.write(render.renderError(message) + '\n');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write rendered output to stdout and exit 0.
|
|
37
|
+
* Checks npm registry for updates at most once per 24h (via cache) and
|
|
38
|
+
* prints a notice to stderr if an update is available. Never throws.
|
|
39
|
+
* @param {string} rendered - Formatted string
|
|
40
|
+
*/
|
|
41
|
+
async function writeAndExit(rendered) {
|
|
42
|
+
process.stdout.write(rendered + '\n');
|
|
43
|
+
// Passive update notification (at most once per 24h via cache)
|
|
44
|
+
try {
|
|
45
|
+
const result = await version.checkForUpdate(process.cwd());
|
|
46
|
+
if (result && result.outdated) {
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`\n Update available: ${result.installedVersion} \u2192 ${result.latestVersion} Run /burrow:update\n\n`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
} catch (_) {
|
|
52
|
+
// Never crash on notification failure
|
|
53
|
+
}
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
const command = process.argv[2];
|
|
59
|
+
const subArgs = process.argv.slice(3);
|
|
60
|
+
const cwd = process.cwd();
|
|
61
|
+
|
|
62
|
+
if (!command) {
|
|
63
|
+
handleError(
|
|
64
|
+
'No command provided. Available: init, add, edit, remove, move, read, dump, path, find, archive, unarchive'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
core.ensureDataDir(cwd);
|
|
69
|
+
|
|
70
|
+
switch (command) {
|
|
71
|
+
case 'init': {
|
|
72
|
+
const result = init(cwd);
|
|
73
|
+
const lines = [
|
|
74
|
+
`gitignore: ${result.gitignore}`,
|
|
75
|
+
`claudeMd: ${result.claudeMd}`,
|
|
76
|
+
`dataDir: ${result.dataDir}`,
|
|
77
|
+
];
|
|
78
|
+
writeAndExit(`Burrow initialized.\n${lines.join('\n')}`);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'add': {
|
|
83
|
+
const { values } = parseArgs({
|
|
84
|
+
args: subArgs,
|
|
85
|
+
options: {
|
|
86
|
+
title: { type: 'string' },
|
|
87
|
+
parent: { type: 'string' },
|
|
88
|
+
body: { type: 'string', default: '' },
|
|
89
|
+
at: { type: 'string' },
|
|
90
|
+
width: { type: 'string' },
|
|
91
|
+
},
|
|
92
|
+
strict: true,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!values.title) {
|
|
96
|
+
handleError('--title is required');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const position = values.at !== undefined ? parseInt(values.at, 10) : undefined;
|
|
100
|
+
if (values.at !== undefined) {
|
|
101
|
+
if (isNaN(position)) handleError('--at must be a number');
|
|
102
|
+
if (position < 0) handleError('--at must be non-negative');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = storage.load(cwd);
|
|
106
|
+
const result = tree.addCard(data, {
|
|
107
|
+
title: values.title,
|
|
108
|
+
parentId: values.parent || null,
|
|
109
|
+
body: values.body,
|
|
110
|
+
position,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!result) {
|
|
114
|
+
handleError('Parent not found');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
storage.save(cwd, data);
|
|
118
|
+
|
|
119
|
+
const rendered = render.renderMutation('add', result.card, {
|
|
120
|
+
breadcrumbs: result.breadcrumbs,
|
|
121
|
+
card: result.card,
|
|
122
|
+
termWidth: resolveTermWidth(values),
|
|
123
|
+
});
|
|
124
|
+
writeAndExit(rendered);
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
case 'edit': {
|
|
129
|
+
const { values, positionals } = parseArgs({
|
|
130
|
+
args: subArgs,
|
|
131
|
+
options: {
|
|
132
|
+
title: { type: 'string' },
|
|
133
|
+
body: { type: 'string' },
|
|
134
|
+
width: { type: 'string' },
|
|
135
|
+
},
|
|
136
|
+
allowPositionals: true,
|
|
137
|
+
strict: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const id = positionals[0];
|
|
141
|
+
if (!id) {
|
|
142
|
+
handleError('Card ID is required');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const data = storage.load(cwd);
|
|
146
|
+
|
|
147
|
+
const result = tree.editCard(data, id, {
|
|
148
|
+
title: values.title,
|
|
149
|
+
body: values.body,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (!result) {
|
|
153
|
+
handleError(`Card not found: ${id}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
storage.save(cwd, data);
|
|
157
|
+
|
|
158
|
+
const rendered = render.renderMutation('edit', result.card, {
|
|
159
|
+
breadcrumbs: result.breadcrumbs,
|
|
160
|
+
card: result.card,
|
|
161
|
+
oldTitle: result.oldTitle,
|
|
162
|
+
oldBody: result.oldBody,
|
|
163
|
+
termWidth: resolveTermWidth(values),
|
|
164
|
+
});
|
|
165
|
+
writeAndExit(rendered);
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
case 'remove': {
|
|
170
|
+
const { positionals } = parseArgs({
|
|
171
|
+
args: subArgs,
|
|
172
|
+
options: {
|
|
173
|
+
width: { type: 'string' },
|
|
174
|
+
},
|
|
175
|
+
allowPositionals: true,
|
|
176
|
+
strict: true,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const id = positionals[0];
|
|
180
|
+
if (!id) {
|
|
181
|
+
handleError('Card ID is required');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const data = storage.load(cwd);
|
|
185
|
+
const result = tree.deleteCard(data, id);
|
|
186
|
+
|
|
187
|
+
if (!result) {
|
|
188
|
+
handleError(`Card not found: ${id}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
storage.save(cwd, data);
|
|
192
|
+
|
|
193
|
+
const rendered = render.renderMutation('remove', result, {});
|
|
194
|
+
writeAndExit(rendered);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'move': {
|
|
199
|
+
const { values, positionals } = parseArgs({
|
|
200
|
+
args: subArgs,
|
|
201
|
+
options: {
|
|
202
|
+
to: { type: 'string' },
|
|
203
|
+
parent: { type: 'string' },
|
|
204
|
+
at: { type: 'string' },
|
|
205
|
+
width: { type: 'string' },
|
|
206
|
+
},
|
|
207
|
+
allowPositionals: true,
|
|
208
|
+
strict: true,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const id = positionals[0];
|
|
212
|
+
if (!id) {
|
|
213
|
+
handleError('Card ID is required');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --to is primary flag, --parent is backward compat
|
|
217
|
+
const rawParent = values.to !== undefined ? values.to : values.parent;
|
|
218
|
+
|
|
219
|
+
const position = values.at !== undefined ? parseInt(values.at, 10) : undefined;
|
|
220
|
+
if (values.at !== undefined) {
|
|
221
|
+
if (isNaN(position)) handleError('--at must be a number');
|
|
222
|
+
if (position < 0) handleError('--at must be non-negative');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const data = storage.load(cwd);
|
|
226
|
+
|
|
227
|
+
// Determine newParentId
|
|
228
|
+
let newParentId;
|
|
229
|
+
if (rawParent === undefined && values.at !== undefined) {
|
|
230
|
+
// Reorder in place: --at without --to means stay in current parent
|
|
231
|
+
const parentResult = tree.findParent(data, id);
|
|
232
|
+
if (!parentResult) {
|
|
233
|
+
handleError('Card not found');
|
|
234
|
+
}
|
|
235
|
+
newParentId = parentResult.parent ? parentResult.parent.id : null;
|
|
236
|
+
} else if (rawParent === undefined) {
|
|
237
|
+
newParentId = null;
|
|
238
|
+
} else if (rawParent === '' || rawParent === 'root') {
|
|
239
|
+
newParentId = null;
|
|
240
|
+
} else {
|
|
241
|
+
newParentId = rawParent;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const result = tree.moveCard(data, id, newParentId, position);
|
|
245
|
+
|
|
246
|
+
if (!result) {
|
|
247
|
+
handleError('Move failed: card not found or would create cycle');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
storage.save(cwd, data);
|
|
251
|
+
|
|
252
|
+
// Get target parent title
|
|
253
|
+
const targetParentTitle = newParentId
|
|
254
|
+
? (tree.findById(data, newParentId) || {}).title || 'unknown'
|
|
255
|
+
: 'root';
|
|
256
|
+
const rendered = render.renderMutation('move', result.card, {
|
|
257
|
+
fromParentTitle: result.sourceParentTitle,
|
|
258
|
+
toParentTitle: targetParentTitle,
|
|
259
|
+
});
|
|
260
|
+
writeAndExit(rendered);
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
case 'read': {
|
|
265
|
+
const { values, positionals } = parseArgs({
|
|
266
|
+
args: subArgs,
|
|
267
|
+
options: {
|
|
268
|
+
depth: { type: 'string' },
|
|
269
|
+
full: { type: 'boolean', default: false },
|
|
270
|
+
'include-archived': { type: 'boolean', default: false },
|
|
271
|
+
'archived-only': { type: 'boolean', default: false },
|
|
272
|
+
width: { type: 'string' },
|
|
273
|
+
},
|
|
274
|
+
allowPositionals: true,
|
|
275
|
+
strict: true,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const id = positionals[0] || null;
|
|
279
|
+
const archiveFilter = values['archived-only']
|
|
280
|
+
? 'archived-only'
|
|
281
|
+
: values['include-archived']
|
|
282
|
+
? 'include-archived'
|
|
283
|
+
: 'active';
|
|
284
|
+
const depth = values.depth !== undefined ? parseInt(values.depth, 10) : 1;
|
|
285
|
+
if (values.depth !== undefined && isNaN(depth)) {
|
|
286
|
+
handleError('--depth must be a number');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const data = storage.load(cwd);
|
|
290
|
+
|
|
291
|
+
if (id) {
|
|
292
|
+
const treeResult = tree.renderTree(data, id, { depth, archiveFilter });
|
|
293
|
+
if (!treeResult || treeResult.cards.length === 0) {
|
|
294
|
+
handleError(`Card not found: ${id}`);
|
|
295
|
+
}
|
|
296
|
+
// treeResult.cards[0] is the root card with nested children already
|
|
297
|
+
const cardToRender = treeResult.cards[0];
|
|
298
|
+
// Merge full body from original card (renderTree only has bodyPreview)
|
|
299
|
+
const fullCard = tree.findById(data, id);
|
|
300
|
+
cardToRender.body = fullCard.body;
|
|
301
|
+
cardToRender.title = fullCard.title;
|
|
302
|
+
const rendered = render.renderCard(cardToRender, treeResult.breadcrumbs || [], {
|
|
303
|
+
full: values.full,
|
|
304
|
+
termWidth: resolveTermWidth(values),
|
|
305
|
+
archiveFilter,
|
|
306
|
+
});
|
|
307
|
+
writeAndExit(rendered);
|
|
308
|
+
} else {
|
|
309
|
+
// Root view: synthesize root card with depth-limited children
|
|
310
|
+
const treeResult = tree.renderTree(data, null, { depth, archiveFilter });
|
|
311
|
+
const rootCard = {
|
|
312
|
+
id: '(root)',
|
|
313
|
+
title: 'burrow',
|
|
314
|
+
created: data.cards[0]?.created || new Date().toISOString(),
|
|
315
|
+
archived: false,
|
|
316
|
+
body: '',
|
|
317
|
+
children: treeResult.cards, // already nested
|
|
318
|
+
};
|
|
319
|
+
const rendered = render.renderCard(rootCard, [], {
|
|
320
|
+
full: values.full,
|
|
321
|
+
termWidth: resolveTermWidth(values),
|
|
322
|
+
archiveFilter,
|
|
323
|
+
});
|
|
324
|
+
writeAndExit(rendered);
|
|
325
|
+
}
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
case 'dump': {
|
|
330
|
+
const { values } = parseArgs({
|
|
331
|
+
args: subArgs,
|
|
332
|
+
options: {
|
|
333
|
+
full: { type: 'boolean', default: true },
|
|
334
|
+
'include-archived': { type: 'boolean', default: false },
|
|
335
|
+
'archived-only': { type: 'boolean', default: false },
|
|
336
|
+
width: { type: 'string' },
|
|
337
|
+
},
|
|
338
|
+
strict: true,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
const archiveFilter = values['archived-only']
|
|
342
|
+
? 'archived-only'
|
|
343
|
+
: values['include-archived']
|
|
344
|
+
? 'include-archived'
|
|
345
|
+
: 'active';
|
|
346
|
+
|
|
347
|
+
const data = storage.load(cwd);
|
|
348
|
+
|
|
349
|
+
// Dump as root card with full tree depth
|
|
350
|
+
const treeResult = tree.renderTree(data, null, { depth: 0, archiveFilter });
|
|
351
|
+
const rootCard = {
|
|
352
|
+
id: '(root)',
|
|
353
|
+
title: 'burrow',
|
|
354
|
+
created: data.cards[0]?.created || new Date().toISOString(),
|
|
355
|
+
archived: false,
|
|
356
|
+
body: '',
|
|
357
|
+
children: treeResult.cards, // already nested
|
|
358
|
+
};
|
|
359
|
+
const rendered = render.renderCard(rootCard, [], {
|
|
360
|
+
full: values.full,
|
|
361
|
+
termWidth: resolveTermWidth(values),
|
|
362
|
+
archiveFilter,
|
|
363
|
+
});
|
|
364
|
+
writeAndExit(rendered);
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
case 'archive': {
|
|
369
|
+
const { positionals } = parseArgs({
|
|
370
|
+
args: subArgs,
|
|
371
|
+
options: {
|
|
372
|
+
width: { type: 'string' },
|
|
373
|
+
},
|
|
374
|
+
allowPositionals: true,
|
|
375
|
+
strict: true,
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const id = positionals[0];
|
|
379
|
+
if (!id) {
|
|
380
|
+
handleError('Card ID is required');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const data = storage.load(cwd);
|
|
384
|
+
const result = tree.archiveCard(data, id);
|
|
385
|
+
|
|
386
|
+
if (!result) {
|
|
387
|
+
handleError(`Card not found: ${id}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
storage.save(cwd, data);
|
|
391
|
+
|
|
392
|
+
const rendered = render.renderMutation('archive', result, {});
|
|
393
|
+
writeAndExit(rendered);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'unarchive': {
|
|
398
|
+
const { positionals } = parseArgs({
|
|
399
|
+
args: subArgs,
|
|
400
|
+
options: {
|
|
401
|
+
width: { type: 'string' },
|
|
402
|
+
},
|
|
403
|
+
allowPositionals: true,
|
|
404
|
+
strict: true,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
const id = positionals[0];
|
|
408
|
+
if (!id) {
|
|
409
|
+
handleError('Card ID is required');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const data = storage.load(cwd);
|
|
413
|
+
const result = tree.unarchiveCard(data, id);
|
|
414
|
+
|
|
415
|
+
if (!result) {
|
|
416
|
+
handleError(`Card not found: ${id}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
storage.save(cwd, data);
|
|
420
|
+
|
|
421
|
+
const rendered = render.renderMutation('unarchive', result, {});
|
|
422
|
+
writeAndExit(rendered);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
case 'path': {
|
|
427
|
+
const { positionals } = parseArgs({
|
|
428
|
+
args: subArgs,
|
|
429
|
+
options: {
|
|
430
|
+
width: { type: 'string' },
|
|
431
|
+
},
|
|
432
|
+
allowPositionals: true,
|
|
433
|
+
strict: true,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const id = positionals[0];
|
|
437
|
+
if (!id) {
|
|
438
|
+
handleError('Card ID is required');
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const data = storage.load(cwd);
|
|
442
|
+
const result = tree.getPath(data, id);
|
|
443
|
+
|
|
444
|
+
if (!result) {
|
|
445
|
+
handleError(`Card not found: ${id}`);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Strip children to keep path output clean
|
|
449
|
+
const cleanPath = result.map((card) => ({ id: card.id, title: card.title }));
|
|
450
|
+
const rendered = render.renderPath(cleanPath);
|
|
451
|
+
writeAndExit(rendered);
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
case 'find': {
|
|
456
|
+
const { positionals: findPositionals } = parseArgs({
|
|
457
|
+
args: subArgs,
|
|
458
|
+
options: {},
|
|
459
|
+
allowPositionals: true,
|
|
460
|
+
strict: true,
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const query = findPositionals.join(' ').trim();
|
|
464
|
+
if (!query) {
|
|
465
|
+
handleError('Search query is required. Usage: find <query>');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const data = storage.load(cwd);
|
|
469
|
+
const matches = tree.searchCards(data, query);
|
|
470
|
+
|
|
471
|
+
if (matches.length === 0) {
|
|
472
|
+
writeAndExit(`No cards matching "${query}"`);
|
|
473
|
+
} else {
|
|
474
|
+
const lines = matches.map((m) => ` ${m.id} ${m.path}`);
|
|
475
|
+
writeAndExit(`Found ${matches.length} match${matches.length === 1 ? '' : 'es'}:\n${lines.join('\n')}`);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
default:
|
|
481
|
+
handleError(
|
|
482
|
+
`Unknown command: ${command}. Available: init, add, edit, remove, move, read, dump, path, find, archive, unarchive`
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
main().catch((err) => {
|
|
488
|
+
process.stdout.write(render.renderError(err.message) + '\n');
|
|
489
|
+
process.exit(1);
|
|
490
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ensure the .planning/burrow/ directory exists.
|
|
9
|
+
* Does NOT create cards.json -- storage.load handles empty state.
|
|
10
|
+
* @param {string} cwd - Working directory
|
|
11
|
+
*/
|
|
12
|
+
function ensureDataDir(cwd) {
|
|
13
|
+
const dir = path.join(cwd, '.planning', 'burrow');
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generate a unique 8-char hex ID.
|
|
19
|
+
* Uses crypto.randomUUID() — collision probability with even 10,000 cards is negligible (~0.000001%).
|
|
20
|
+
* @returns {string} 8-char hex string
|
|
21
|
+
*/
|
|
22
|
+
function generateId() {
|
|
23
|
+
return crypto.randomUUID().replace(/-/g, '').slice(0, 8);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { ensureDataDir, generateId };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { ensureDataDir } = require('./core.cjs');
|
|
6
|
+
|
|
7
|
+
const GITIGNORE_ENTRY = '.planning/burrow/cards.json.bak';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Burrow section to append to CLAUDE.md.
|
|
11
|
+
* Uses LF line endings by default; caller normalizes to match the file.
|
|
12
|
+
*/
|
|
13
|
+
const BURROW_SECTION_LF = `\n## Burrow -- Agent Memory\n\nBurrow is your persistent memory. On every session start, silently read \`.planning/burrow/cards.json\` using the Read tool to load project context, tasks, and notes.\n\n**When the user says "remember", "don't forget", "always do X", or any instruction meant to persist across sessions:**\n- Store it as a Burrow card: \`node .claude/burrow/burrow-tools.cjs add --title "<summary>" --body "<details>"\`\n- Organize under relevant parent cards (create parents if needed)\n- Do NOT write to loose markdown files or other ad-hoc storage — Burrow is the single source of truth\n\n**Privacy:** Burrow data is meant to be committed to git. Anything stored in cards is visible to anyone with repo access. Avoid storing secrets, credentials, or sensitive personal information.\n\nAll mutations go through the CLI — NEVER edit cards.json directly.\n`;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert LF line endings to CRLF.
|
|
17
|
+
* @param {string} str
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
function toCRLF(str) {
|
|
21
|
+
return str.replace(/\r?\n/g, '\r\n');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialize Burrow in a project directory.
|
|
26
|
+
*
|
|
27
|
+
* Actions:
|
|
28
|
+
* 1. Ensure .planning/burrow/ data directory exists
|
|
29
|
+
* 2. Add .planning/burrow/cards.json.bak to .gitignore (if not already present)
|
|
30
|
+
* 3. Append Burrow instructions section to CLAUDE.md (if not already present),
|
|
31
|
+
* matching existing file line endings (LF or CRLF)
|
|
32
|
+
*
|
|
33
|
+
* @param {string} cwd - Target project directory
|
|
34
|
+
* @returns {{ gitignore: 'created'|'updated'|'unchanged', claudeMd: 'created'|'updated'|'unchanged', dataDir: 'created'|'existed' }}
|
|
35
|
+
*/
|
|
36
|
+
function init(cwd) {
|
|
37
|
+
// 1. Data directory
|
|
38
|
+
const dataDirPath = path.join(cwd, '.planning', 'burrow');
|
|
39
|
+
const dataDirExisted = fs.existsSync(dataDirPath);
|
|
40
|
+
ensureDataDir(cwd);
|
|
41
|
+
const dataDirResult = dataDirExisted ? 'existed' : 'created';
|
|
42
|
+
|
|
43
|
+
// 2. .gitignore handling
|
|
44
|
+
const gitignorePath = path.join(cwd, '.gitignore');
|
|
45
|
+
let gitignoreResult;
|
|
46
|
+
|
|
47
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
48
|
+
// Create with just the entry
|
|
49
|
+
fs.writeFileSync(gitignorePath, GITIGNORE_ENTRY + '\n', 'utf-8');
|
|
50
|
+
gitignoreResult = 'created';
|
|
51
|
+
} else {
|
|
52
|
+
const existing = fs.readFileSync(gitignorePath, 'utf-8');
|
|
53
|
+
const lines = existing.split(/\r?\n/).map((l) => l.trim());
|
|
54
|
+
if (lines.includes(GITIGNORE_ENTRY)) {
|
|
55
|
+
gitignoreResult = 'unchanged';
|
|
56
|
+
} else {
|
|
57
|
+
// Append with a preceding newline if file doesn't already end with newline
|
|
58
|
+
const separator = existing.endsWith('\n') ? '' : '\n';
|
|
59
|
+
fs.writeFileSync(gitignorePath, existing + separator + GITIGNORE_ENTRY + '\n', 'utf-8');
|
|
60
|
+
gitignoreResult = 'updated';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. CLAUDE.md handling
|
|
65
|
+
const claudeMdPath = path.join(cwd, 'CLAUDE.md');
|
|
66
|
+
let claudeMdResult;
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(claudeMdPath)) {
|
|
69
|
+
// Create with Burrow section (LF endings)
|
|
70
|
+
fs.writeFileSync(claudeMdPath, BURROW_SECTION_LF.trimStart(), 'utf-8');
|
|
71
|
+
claudeMdResult = 'created';
|
|
72
|
+
} else {
|
|
73
|
+
const existing = fs.readFileSync(claudeMdPath, 'utf-8');
|
|
74
|
+
if (existing.includes('## Burrow')) {
|
|
75
|
+
claudeMdResult = 'unchanged';
|
|
76
|
+
} else {
|
|
77
|
+
// Detect line endings: CRLF if file has \r\n
|
|
78
|
+
const isCRLF = existing.includes('\r\n');
|
|
79
|
+
const section = isCRLF ? toCRLF(BURROW_SECTION_LF) : BURROW_SECTION_LF;
|
|
80
|
+
// Ensure file ends with a newline before appending
|
|
81
|
+
const eol = isCRLF ? '\r\n' : '\n';
|
|
82
|
+
const separator = existing.endsWith('\n') ? '' : eol;
|
|
83
|
+
fs.writeFileSync(claudeMdPath, existing + separator + section, 'utf-8');
|
|
84
|
+
claudeMdResult = 'updated';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
gitignore: gitignoreResult,
|
|
90
|
+
claudeMd: claudeMdResult,
|
|
91
|
+
dataDir: dataDirResult,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { init };
|