@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.
- package/.obsidian/.2ndbrain-manifest.json +8 -0
- package/.obsidian/app.json +6 -0
- package/.obsidian/appearance.json +1 -0
- package/.obsidian/community-plugins.json +4 -0
- package/.obsidian/core-plugins.json +33 -0
- package/.obsidian/graph.json +22 -0
- package/.obsidian/plugins/calendar/data.json +10 -0
- package/.obsidian/plugins/calendar/main.js +4459 -0
- package/.obsidian/plugins/calendar/manifest.json +10 -0
- package/.obsidian/plugins/obsidian-custom-attachment-location/data.json +32 -0
- package/.obsidian/plugins/obsidian-custom-attachment-location/main.js +575 -0
- package/.obsidian/plugins/obsidian-custom-attachment-location/manifest.json +11 -0
- package/.obsidian/plugins/obsidian-custom-attachment-location/styles.css +1 -0
- package/.obsidian/plugins/obsidian-git/data.json +62 -0
- package/.obsidian/plugins/obsidian-git/main.js +426 -0
- package/.obsidian/plugins/obsidian-git/manifest.json +10 -0
- package/.obsidian/plugins/obsidian-git/obsidian_askpass.sh +23 -0
- package/.obsidian/plugins/obsidian-git/styles.css +629 -0
- package/.obsidian/plugins/obsidian-tasks-plugin/main.js +504 -0
- package/.obsidian/plugins/obsidian-tasks-plugin/manifest.json +12 -0
- package/.obsidian/plugins/obsidian-tasks-plugin/styles.css +1 -0
- package/.obsidian/types.json +28 -0
- package/00_Dashboard/01_All_Tasks.md +118 -0
- package/00_Dashboard/09_All_Done.md +42 -0
- package/10_Inbox/Agents/Journal.md +1 -0
- package/99_System/Scripts/init_member.sh +108 -0
- package/99_System/Templates/tpl_daily_note.md +13 -0
- package/99_System/Templates/tpl_member_done.md +32 -0
- package/99_System/Templates/tpl_member_tasks.md +97 -0
- package/AGENTS.md +193 -0
- package/CHANGELOG.md +67 -0
- package/CLAUDE.md +153 -0
- package/LICENSE +201 -0
- package/README.md +636 -0
- package/bin/2ndbrain.js +117 -0
- package/package.json +56 -0
- package/src/commands/completion.js +198 -0
- package/src/commands/init.js +308 -0
- package/src/commands/member.js +123 -0
- package/src/commands/remove.js +88 -0
- package/src/commands/update.js +507 -0
- package/src/index.js +17 -0
- package/src/lib/config.js +112 -0
- package/src/lib/diff.js +222 -0
- package/src/lib/files.js +340 -0
- package/src/lib/obsidian.js +366 -0
- 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
|
+
};
|