@our2ndbrain/cli 1.1.3

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.
Files changed (47) hide show
  1. package/.obsidian/.2ndbrain-manifest.json +8 -0
  2. package/.obsidian/app.json +6 -0
  3. package/.obsidian/appearance.json +1 -0
  4. package/.obsidian/community-plugins.json +4 -0
  5. package/.obsidian/core-plugins.json +33 -0
  6. package/.obsidian/graph.json +22 -0
  7. package/.obsidian/plugins/calendar/data.json +10 -0
  8. package/.obsidian/plugins/calendar/main.js +4459 -0
  9. package/.obsidian/plugins/calendar/manifest.json +10 -0
  10. package/.obsidian/plugins/obsidian-custom-attachment-location/data.json +32 -0
  11. package/.obsidian/plugins/obsidian-custom-attachment-location/main.js +575 -0
  12. package/.obsidian/plugins/obsidian-custom-attachment-location/manifest.json +11 -0
  13. package/.obsidian/plugins/obsidian-custom-attachment-location/styles.css +1 -0
  14. package/.obsidian/plugins/obsidian-git/data.json +62 -0
  15. package/.obsidian/plugins/obsidian-git/main.js +426 -0
  16. package/.obsidian/plugins/obsidian-git/manifest.json +10 -0
  17. package/.obsidian/plugins/obsidian-git/obsidian_askpass.sh +23 -0
  18. package/.obsidian/plugins/obsidian-git/styles.css +629 -0
  19. package/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
  20. package/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
  21. package/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
  22. package/.obsidian/types.json +28 -0
  23. package/00_Dashboard/01_All_Tasks.md +118 -0
  24. package/00_Dashboard/09_All_Done.md +42 -0
  25. package/10_Inbox/Agents/Journal.md +1 -0
  26. package/99_System/Scripts/init_member.sh +108 -0
  27. package/99_System/Templates/tpl_daily_note.md +13 -0
  28. package/99_System/Templates/tpl_member_done.md +32 -0
  29. package/99_System/Templates/tpl_member_tasks.md +97 -0
  30. package/AGENTS.md +193 -0
  31. package/CHANGELOG.md +67 -0
  32. package/CLAUDE.md +153 -0
  33. package/LICENSE +201 -0
  34. package/README.md +636 -0
  35. package/bin/2ndbrain.js +117 -0
  36. package/package.json +56 -0
  37. package/src/commands/completion.js +198 -0
  38. package/src/commands/init.js +308 -0
  39. package/src/commands/member.js +123 -0
  40. package/src/commands/remove.js +88 -0
  41. package/src/commands/update.js +507 -0
  42. package/src/index.js +17 -0
  43. package/src/lib/config.js +112 -0
  44. package/src/lib/diff.js +222 -0
  45. package/src/lib/files.js +340 -0
  46. package/src/lib/obsidian.js +366 -0
  47. package/src/lib/prompt.js +182 -0
@@ -0,0 +1,507 @@
1
+ /**
2
+ * 2ndBrain CLI - Update Command
3
+ *
4
+ * Update framework files from template with diff display and confirmation.
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const chalk = require('chalk');
10
+ const {
11
+ TEMPLATE_ROOT,
12
+ FRAMEWORK_FILES,
13
+ SMART_COPY_DIRS,
14
+ is2ndBrainProject,
15
+ } = require('../lib/config');
16
+ const { copyFilesSmart, copyFileWithCompare, createFile } = require('../lib/files');
17
+ const { generateDiff, formatDiffForTerminal, areContentsEqual, isBinaryFile, isLargeFile, LARGE_FILE_THRESHOLD } = require('../lib/diff');
18
+ const { confirm, confirmBatchUpdates, confirmFile, confirmBinaryFile, confirmLargeFile } = require('../lib/prompt');
19
+ const { copyObsidianDirSmart, MERGE_STRATEGIES } = require('../lib/obsidian');
20
+
21
+ /**
22
+ * Update framework files in an existing 2ndBrain project
23
+ * @param {string} targetPath - Target directory path
24
+ * @param {Object} options - Command options
25
+ * @param {string} [options.template] - Custom template path
26
+ * @param {boolean} [options.dryRun] - Dry run mode
27
+ * @param {boolean} [options.yes] - Auto-confirm all updates
28
+ * @param {Function} log - Logger function
29
+ */
30
+ async function update(targetPath, options, log) {
31
+ const resolvedPath = path.resolve(targetPath);
32
+ const templateRoot = options.template ? path.resolve(options.template) : TEMPLATE_ROOT;
33
+
34
+ log.info(`Updating 2ndBrain project at: ${resolvedPath}`);
35
+ log.info(`Using template from: ${templateRoot}`);
36
+
37
+ // Check if target is a 2ndBrain project
38
+ if (!is2ndBrainProject(resolvedPath)) {
39
+ throw new Error(
40
+ 'Target directory is not a 2ndBrain project. Run "2ndbrain init" first.'
41
+ );
42
+ }
43
+
44
+ if (options.dryRun) {
45
+ await performDryRun(resolvedPath, templateRoot, log);
46
+ return;
47
+ }
48
+
49
+ // Main update flow
50
+ await performUpdate(resolvedPath, templateRoot, options, log);
51
+ }
52
+
53
+ /**
54
+ * Perform dry run - show what would be updated
55
+ * @param {string} resolvedPath - Target project path
56
+ * @param {string} templateRoot - Template root path
57
+ * @param {Function} log - Logger function
58
+ */
59
+ async function performDryRun(resolvedPath, templateRoot, log) {
60
+ log.warn('[DRY RUN] No files will be modified.');
61
+ log.info('');
62
+
63
+ log.info('Analyzing framework files...');
64
+
65
+ const result = await copyFilesSmart(
66
+ FRAMEWORK_FILES,
67
+ templateRoot,
68
+ resolvedPath,
69
+ { dryRun: true },
70
+ (file, action, detail) => {
71
+ if (action === 'unchanged') {
72
+ log.info(` = ${file}`);
73
+ } else if (action === 'dryrun' && detail) {
74
+ const parts = [];
75
+ if (detail.binary) {
76
+ parts.push('binary');
77
+ } else if (detail.large) {
78
+ parts.push('large file');
79
+ } else {
80
+ if (detail.added > 0) parts.push(chalk.green(`+${detail.added}`));
81
+ if (detail.removed > 0) parts.push(chalk.red(`-${detail.removed}`));
82
+ }
83
+ log.info(` * ${file} (${parts.join(' ')})`);
84
+ }
85
+ }
86
+ );
87
+
88
+ // Summary
89
+ log.info('');
90
+ log.info('Framework files summary:');
91
+ log.info(` Unchanged: ${result.unchanged.length} files`);
92
+ log.info(` Would update: ${result.skipped.length} files`);
93
+
94
+ // Analyze .obsidian directory
95
+ const obsidianTemplatePath = path.join(templateRoot, '.obsidian');
96
+ const obsidianTargetPath = path.join(resolvedPath, '.obsidian');
97
+
98
+ if (await fs.pathExists(obsidianTemplatePath)) {
99
+ log.info('');
100
+ log.info('Analyzing .obsidian directory...');
101
+
102
+ const obsidianResult = await copyObsidianDirSmart(
103
+ obsidianTemplatePath,
104
+ obsidianTargetPath,
105
+ { dryRun: true }
106
+ );
107
+
108
+ // Display .obsidian changes
109
+ for (const item of obsidianResult.added) {
110
+ log.info(` + ${item.file}`);
111
+ }
112
+ for (const item of obsidianResult.updated) {
113
+ log.info(` ↻ ${item.file}`);
114
+ }
115
+ for (const item of obsidianResult.merged) {
116
+ const parts = [];
117
+ if (item.added && item.added.length > 0) {
118
+ parts.push(chalk.green(`+${item.added.join(', ')}`));
119
+ }
120
+ if (item.removed && item.removed.length > 0) {
121
+ parts.push(chalk.red(`-${item.removed.join(', ')}`));
122
+ }
123
+ log.info(` ↻ ${item.file} (${parts.join(' ')})`);
124
+ }
125
+ for (const item of obsidianResult.unchanged) {
126
+ log.info(` = ${item.file}`);
127
+ }
128
+ for (const item of obsidianResult.preserved) {
129
+ log.info(` = ${item.file} (preserved)`);
130
+ }
131
+
132
+ // Summary
133
+ log.info('');
134
+ log.info('.obsidian summary:');
135
+ log.info(` New files: ${obsidianResult.added.length}`);
136
+ log.info(` Updated: ${obsidianResult.updated.length}`);
137
+ log.info(` Merged: ${obsidianResult.merged.length}`);
138
+ log.info(` Unchanged: ${obsidianResult.unchanged.length}`);
139
+ log.info(` Preserved: ${obsidianResult.preserved.length}`);
140
+ }
141
+
142
+ // Member dashboards
143
+ const inboxDir = path.join(resolvedPath, '10_Inbox');
144
+ if (await fs.pathExists(inboxDir)) {
145
+ const members = await getMemberDirectories(inboxDir);
146
+ if (members.length > 0) {
147
+ log.info('');
148
+ log.info('Member dashboards that would be updated:');
149
+ for (const member of members) {
150
+ log.info(` 10_Inbox/${member}/01_Tasks.md`);
151
+ log.info(` 10_Inbox/${member}/09_Done.md`);
152
+ }
153
+ }
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Perform the actual update with user confirmation
159
+ * @param {string} resolvedPath - Target project path
160
+ * @param {string} templateRoot - Template root path
161
+ * @param {Object} options - Command options
162
+ * @param {boolean} [options.yes] - Auto-confirm all updates
163
+ * @param {Function} log - Logger function
164
+ */
165
+ async function performUpdate(resolvedPath, templateRoot, options, log) {
166
+ log.info('Analyzing framework files...');
167
+
168
+ // First pass: analyze all files
169
+ const analysis = await copyFilesSmart(
170
+ FRAMEWORK_FILES,
171
+ templateRoot,
172
+ resolvedPath,
173
+ {},
174
+ (file, action, detail) => {
175
+ // Silent during analysis
176
+ }
177
+ );
178
+
179
+ // Display analysis results
180
+ for (const file of analysis.unchanged) {
181
+ log.info(` = ${file} (unchanged)`);
182
+ }
183
+
184
+ // Filter out unchanged files from changes
185
+ const changes = analysis.changes.filter(c => {
186
+ const file = c.file;
187
+ return !analysis.unchanged.includes(file);
188
+ });
189
+
190
+ if (changes.length === 0 && analysis.unchanged.length > 0) {
191
+ log.info('');
192
+ log.success('All framework files are already up to date!');
193
+ return;
194
+ }
195
+
196
+ // Ask user what to do
197
+ let userChoice;
198
+ if (options.yes) {
199
+ userChoice = 'all';
200
+ } else {
201
+ userChoice = await confirmBatchUpdates(changes, log, chalk);
202
+
203
+ if (userChoice === 'skip') {
204
+ log.info('Update cancelled.');
205
+ return;
206
+ }
207
+ }
208
+
209
+ // Execute updates
210
+ log.info('');
211
+ log.info('Updating framework files...');
212
+
213
+ if (userChoice === 'all') {
214
+ // Apply all changes
215
+ for (const change of changes) {
216
+ const src = path.join(templateRoot, change.file);
217
+ const dest = path.join(resolvedPath, change.file);
218
+
219
+ // Handle special cases
220
+ if (change.large && !options.yes) {
221
+ const stats = await fs.stat(src);
222
+ const confirmed = await confirmLargeFile(change.file, stats.size);
223
+ if (!confirmed) {
224
+ log.warn(` ~ ${change.file} (skipped)`);
225
+ continue;
226
+ }
227
+ } else if (change.binary && !options.yes) {
228
+ const confirmed = await confirmBinaryFile(change.file);
229
+ if (!confirmed) {
230
+ log.warn(` ~ ${change.file} (skipped)`);
231
+ continue;
232
+ }
233
+ }
234
+
235
+ await copyFileWithCompare(src, dest, { force: true });
236
+ log.success(` ↻ ${change.file}`);
237
+ }
238
+ } else if (userChoice === 'review') {
239
+ // Review each file individually
240
+ for (const change of changes) {
241
+ const file = change.file;
242
+ const src = path.join(templateRoot, file);
243
+ const dest = path.join(resolvedPath, file);
244
+
245
+ // Handle special files
246
+ if (change.large) {
247
+ const stats = await fs.stat(src);
248
+ const confirmed = await confirmLargeFile(file, stats.size);
249
+ if (!confirmed) {
250
+ log.warn(` ~ ${file} (skipped)`);
251
+ continue;
252
+ }
253
+ await copyFileWithCompare(src, dest, { force: true });
254
+ log.success(` ↻ ${file}`);
255
+ continue;
256
+ }
257
+
258
+ if (change.binary) {
259
+ const confirmed = await confirmBinaryFile(file);
260
+ if (!confirmed) {
261
+ log.warn(` ~ ${file} (skipped)`);
262
+ continue;
263
+ }
264
+ await copyFileWithCompare(src, dest, { force: true });
265
+ log.success(` ↻ ${file}`);
266
+ continue;
267
+ }
268
+
269
+ // Show diff for text files
270
+ console.log('');
271
+ console.log(chalk.bold(`=== ${file} ===`));
272
+
273
+ const [oldContent, newContent] = await Promise.all([
274
+ fs.readFile(dest, 'utf8').catch(() => ''),
275
+ fs.readFile(src, 'utf8'),
276
+ ]);
277
+
278
+ const diffText = generateDiff(oldContent, newContent, file, file);
279
+ const formattedDiff = formatDiffForTerminal(diffText, chalk);
280
+ console.log(formattedDiff);
281
+
282
+ const shouldUpdate = await confirmFile(file, true);
283
+ if (shouldUpdate) {
284
+ await copyFileWithCompare(src, dest, { force: true });
285
+ log.success(` ↻ ${file}`);
286
+ } else {
287
+ log.warn(` ~ ${file} (skipped)`);
288
+ }
289
+ }
290
+ }
291
+
292
+ // Summary for framework files
293
+ log.info('');
294
+ log.success('Framework files updated!');
295
+
296
+ // Update .obsidian directory with smart merge
297
+ await updateObsidianDir(resolvedPath, templateRoot, log);
298
+
299
+ // Now update member dashboards
300
+ await updateMemberDashboards(resolvedPath, templateRoot, options, log);
301
+ }
302
+
303
+ /**
304
+ * Update .obsidian directory with smart merge strategies
305
+ * @param {string} resolvedPath - Target project path
306
+ * @param {string} templateRoot - Template root path
307
+ * @param {Function} log - Logger function
308
+ */
309
+ async function updateObsidianDir(resolvedPath, templateRoot, log) {
310
+ const obsidianTemplatePath = path.join(templateRoot, '.obsidian');
311
+ const obsidianTargetPath = path.join(resolvedPath, '.obsidian');
312
+
313
+ if (!(await fs.pathExists(obsidianTemplatePath))) {
314
+ return;
315
+ }
316
+
317
+ log.info('');
318
+ log.info('Updating .obsidian directory...');
319
+
320
+ const result = await copyObsidianDirSmart(
321
+ obsidianTemplatePath,
322
+ obsidianTargetPath,
323
+ { dryRun: false }
324
+ );
325
+
326
+ // Display results
327
+ for (const item of result.added) {
328
+ log.success(` + ${item.file}`);
329
+ }
330
+ for (const item of result.updated) {
331
+ log.success(` ↻ ${item.file}`);
332
+ }
333
+ for (const item of result.merged) {
334
+ const parts = [];
335
+ if (item.added && item.added.length > 0) {
336
+ parts.push(chalk.green(`+${item.added.join(', ')}`));
337
+ }
338
+ if (item.removed && item.removed.length > 0) {
339
+ parts.push(chalk.red(`-${item.removed.join(', ')}`));
340
+ }
341
+ log.success(` ↻ ${item.file} (${parts.join(' ')})`);
342
+ }
343
+ for (const item of result.unchanged) {
344
+ log.info(` = ${item.file}`);
345
+ }
346
+ for (const item of result.preserved) {
347
+ log.info(` = ${item.file} (preserved)`);
348
+ }
349
+
350
+ // Summary
351
+ log.info('');
352
+ log.success('.obsidian directory updated!');
353
+ log.info(` New files: ${result.added.length}`);
354
+ log.info(` Updated: ${result.updated.length}`);
355
+ log.info(` Merged: ${result.merged.length}`);
356
+ log.info(` Unchanged: ${result.unchanged.length}`);
357
+ log.info(` Preserved: ${result.preserved.length}`);
358
+ }
359
+
360
+ /**
361
+ * Update all member dashboard files using latest templates
362
+ * @param {string} resolvedPath - Target project path
363
+ * @param {string} templateRoot - Template root path
364
+ * @param {Object} options - Command options
365
+ * @param {boolean} [options.yes] - Auto-confirm all updates
366
+ * @param {Function} log - Logger function
367
+ */
368
+ async function updateMemberDashboards(resolvedPath, templateRoot, options, log) {
369
+ const inboxDir = path.join(resolvedPath, '10_Inbox');
370
+ const templateDir = path.join(templateRoot, '99_System/Templates');
371
+
372
+ if (!(await fs.pathExists(inboxDir))) {
373
+ return;
374
+ }
375
+
376
+ const members = await getMemberDirectories(inboxDir);
377
+
378
+ if (members.length === 0) {
379
+ return;
380
+ }
381
+
382
+ log.info('');
383
+ log.info('Updating member dashboards...');
384
+
385
+ const PLACEHOLDER = '{{MEMBER_NAME}}';
386
+ const memberChanges = [];
387
+
388
+ for (const member of members) {
389
+ const memberDir = path.join(inboxDir, member);
390
+ const filesToCheck = [
391
+ { template: 'tpl_member_tasks.md', output: '01_Tasks.md' },
392
+ { template: 'tpl_member_done.md', output: '09_Done.md' },
393
+ ];
394
+
395
+ for (const { template, output } of filesToCheck) {
396
+ const templatePath = path.join(templateDir, template);
397
+ const outputPath = path.join(memberDir, output);
398
+
399
+ if (!(await fs.pathExists(templatePath))) {
400
+ continue;
401
+ }
402
+
403
+ try {
404
+ let newContent = await fs.readFile(templatePath, 'utf8');
405
+ newContent = newContent.replace(new RegExp(PLACEHOLDER, 'g'), member);
406
+
407
+ const oldContent = await fs.readFile(outputPath, 'utf8').catch(() => '');
408
+
409
+ if (!areContentsEqual(oldContent, newContent)) {
410
+ const summary = require('../lib/diff').summarizeChanges(oldContent, newContent);
411
+ memberChanges.push({
412
+ file: `10_Inbox/${member}/${output}`,
413
+ added: summary.added,
414
+ removed: summary.removed,
415
+ outputPath,
416
+ newContent,
417
+ });
418
+ } else {
419
+ log.info(` = 10_Inbox/${member}/${output} (unchanged)`);
420
+ }
421
+ } catch (err) {
422
+ log.error(` ! 10_Inbox/${member}/${output} (error: ${err.message})`);
423
+ }
424
+ }
425
+ }
426
+
427
+ if (memberChanges.length === 0) {
428
+ return;
429
+ }
430
+
431
+ // Ask about member dashboard updates
432
+ let memberChoice;
433
+ if (options.yes) {
434
+ memberChoice = 'all';
435
+ } else {
436
+ memberChoice = await confirmBatchUpdates(memberChanges, log, chalk);
437
+
438
+ if (memberChoice === 'skip') {
439
+ log.info('Member dashboard updates cancelled.');
440
+ return;
441
+ }
442
+ }
443
+
444
+ // Execute member dashboard updates
445
+ if (memberChoice === 'all') {
446
+ for (const change of memberChanges) {
447
+ await createFile(change.outputPath, change.newContent);
448
+ log.success(` ↻ ${change.file}`);
449
+ }
450
+ } else if (memberChoice === 'review') {
451
+ for (const change of memberChanges) {
452
+ console.log('');
453
+ console.log(chalk.bold(`=== ${change.file} ===`));
454
+
455
+ const oldContent = await fs.readFile(change.outputPath, 'utf8').catch(() => '');
456
+ const diffText = generateDiff(oldContent, change.newContent, change.file, change.file);
457
+ const formattedDiff = formatDiffForTerminal(diffText, chalk);
458
+ console.log(formattedDiff);
459
+
460
+ const shouldUpdate = await confirmFile(change.file, true);
461
+ if (shouldUpdate) {
462
+ await createFile(change.outputPath, change.newContent);
463
+ log.success(` ↻ ${change.file}`);
464
+ } else {
465
+ log.warn(` ~ ${change.file} (skipped)`);
466
+ }
467
+ }
468
+ }
469
+
470
+ log.success('Member dashboards updated!');
471
+ }
472
+
473
+ /**
474
+ * Get all member directories from 10_Inbox (excluding 'Agents')
475
+ * @param {string} inboxDir - Path to 10_Inbox directory
476
+ * @returns {Promise<string[]>} Array of member directory names
477
+ */
478
+ async function getMemberDirectories(inboxDir) {
479
+ let entries;
480
+ try {
481
+ entries = await fs.readdir(inboxDir);
482
+ } catch (err) {
483
+ // If directory cannot be read, return empty array
484
+ return [];
485
+ }
486
+
487
+ const members = [];
488
+
489
+ for (const entry of entries) {
490
+ const entryPath = path.join(inboxDir, entry);
491
+
492
+ try {
493
+ const stat = await fs.stat(entryPath);
494
+
495
+ if (stat.isDirectory() && entry !== 'Agents' && !entry.startsWith('.')) {
496
+ members.push(entry);
497
+ }
498
+ } catch (err) {
499
+ // Skip entries that cannot be accessed (broken symlinks, permission issues, etc.)
500
+ continue;
501
+ }
502
+ }
503
+
504
+ return members;
505
+ }
506
+
507
+ module.exports = update;
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * 2ndBrain CLI - Main Entry
3
+ */
4
+
5
+ const init = require('./commands/init');
6
+ const update = require('./commands/update');
7
+ const remove = require('./commands/remove');
8
+ const member = require('./commands/member');
9
+ const completion = require('./commands/completion');
10
+
11
+ module.exports = {
12
+ init,
13
+ update,
14
+ remove,
15
+ member,
16
+ completion,
17
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * 2ndBrain CLI - Configuration
3
+ *
4
+ * Defines framework files and directories managed by the CLI tool.
5
+ */
6
+
7
+ const path = require('path');
8
+
9
+ // Template root directory (npm package root)
10
+ const TEMPLATE_ROOT = path.resolve(__dirname, '../../');
11
+
12
+ /**
13
+ * Framework files - managed by init/update/remove commands
14
+ * These files will be copied during init, updated during update, and removed during remove.
15
+ */
16
+ const FRAMEWORK_FILES = [
17
+ 'AGENTS.md',
18
+ 'README.md',
19
+ 'CHANGELOG.md',
20
+ 'CLAUDE.md',
21
+ 'LICENSE',
22
+ '00_Dashboard/01_All_Tasks.md',
23
+ '00_Dashboard/09_All_Done.md',
24
+ '99_System/Templates/tpl_daily_note.md',
25
+ '99_System/Templates/tpl_member_tasks.md',
26
+ '99_System/Templates/tpl_member_done.md',
27
+ '99_System/Scripts/init_member.sh',
28
+ ];
29
+
30
+ /**
31
+ * Framework directories - created during init
32
+ */
33
+ const FRAMEWORK_DIRS = [
34
+ '00_Dashboard',
35
+ '10_Inbox/Agents',
36
+ '99_System/Templates',
37
+ '99_System/Scripts',
38
+ ];
39
+
40
+ /**
41
+ * User data directories - NEVER touched by update/remove
42
+ */
43
+ const USER_DATA_DIRS = [
44
+ '20_Areas',
45
+ '30_Projects',
46
+ '40_Resources',
47
+ '90_Archives',
48
+ ];
49
+
50
+ /**
51
+ * Directories to copy entirely during init (e.g., .obsidian with plugins)
52
+ */
53
+ const COPY_DIRS = [
54
+ '.obsidian',
55
+ ];
56
+
57
+ /**
58
+ * Directories requiring smart merge (preserves user configs, adds new items)
59
+ */
60
+ const SMART_COPY_DIRS = [
61
+ '.obsidian',
62
+ ];
63
+
64
+ /**
65
+ * Files created only during init (not updated/removed)
66
+ */
67
+ const INIT_ONLY_FILES = [
68
+ { path: '10_Inbox/Agents/Journal.md', content: '# Agent Journal\n' },
69
+ ];
70
+
71
+ /**
72
+ * Marker file to identify a 2ndBrain project
73
+ */
74
+ const MARKER_FILE = 'AGENTS.md';
75
+
76
+ /**
77
+ * Get the path to a template file
78
+ * @param {string} relativePath - Relative path from template root
79
+ * @param {string} [templateRoot] - Optional custom template root
80
+ * @returns {string} Absolute path to the template file
81
+ */
82
+ function getTemplatePath(relativePath, templateRoot = TEMPLATE_ROOT) {
83
+ return path.join(templateRoot, relativePath);
84
+ }
85
+
86
+ /**
87
+ * Check if a path is a 2ndBrain project
88
+ * @param {string} targetPath - Path to check
89
+ * @returns {boolean}
90
+ */
91
+ function is2ndBrainProject(targetPath) {
92
+ const markerPath = path.join(targetPath, MARKER_FILE);
93
+ try {
94
+ require('fs').accessSync(markerPath);
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ module.exports = {
102
+ TEMPLATE_ROOT,
103
+ FRAMEWORK_FILES,
104
+ FRAMEWORK_DIRS,
105
+ USER_DATA_DIRS,
106
+ COPY_DIRS,
107
+ SMART_COPY_DIRS,
108
+ INIT_ONLY_FILES,
109
+ MARKER_FILE,
110
+ getTemplatePath,
111
+ is2ndBrainProject,
112
+ };