@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,117 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * 2ndBrain CLI
5
+ *
6
+ * A CLI tool for managing 2ndBrain projects.
7
+ */
8
+
9
+ const { program } = require('commander');
10
+ const { init, update, remove, member, completion } = require('../src');
11
+ const pkg = require('../package.json');
12
+
13
+ // ANSI color codes for terminal output
14
+ const colors = {
15
+ green: (str) => `\x1b[32m${str}\x1b[0m`,
16
+ yellow: (str) => `\x1b[33m${str}\x1b[0m`,
17
+ red: (str) => `\x1b[31m${str}\x1b[0m`,
18
+ };
19
+
20
+ // Logger utility
21
+ const log = {
22
+ info: (msg) => console.log(msg),
23
+ success: (msg) => console.log(colors.green(msg)),
24
+ warn: (msg) => console.log(colors.yellow(msg)),
25
+ error: (msg) => console.log(colors.red(msg)),
26
+ };
27
+
28
+ // CLI setup
29
+ program
30
+ .name('2ndbrain')
31
+ .description('CLI tool for 2ndBrain - A personal knowledge management system')
32
+ .version(pkg.version);
33
+
34
+ // Init command
35
+ program
36
+ .command('init [path]')
37
+ .description('Initialize a new 2ndBrain project (or integrate into existing vault)')
38
+ .option('-t, --template <path>', 'Use custom template directory')
39
+ .option('-f, --force', 'Force overwrite existing 2ndBrain project')
40
+ .option('--reset-obsidian, --reset-obs', 'Reset .obsidian directory from template (overwrites all settings)')
41
+ .action(async (targetPath = '.', options) => {
42
+ try {
43
+ await init(targetPath, options, log);
44
+ } catch (err) {
45
+ log.error(`Error: ${err.message}`);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ // Update command
51
+ program
52
+ .command('update [path]')
53
+ .description('Update framework files from template')
54
+ .option('-t, --template <path>', 'Use custom template directory')
55
+ .option('-d, --dry-run', 'Show what would be updated without making changes')
56
+ .option('-y, --yes', 'Auto-confirm all updates without prompting')
57
+ .action(async (targetPath = '.', options) => {
58
+ try {
59
+ await update(targetPath, options, log);
60
+ } catch (err) {
61
+ log.error(`Error: ${err.message}`);
62
+ process.exit(1);
63
+ }
64
+ });
65
+
66
+ // Remove command
67
+ program
68
+ .command('remove [path]')
69
+ .description('Remove framework files (preserves user data)')
70
+ .option('-d, --dry-run', 'Show what would be removed without making changes')
71
+ .option('-f, --force', 'Force removal without confirmation')
72
+ .action(async (targetPath = '.', options) => {
73
+ try {
74
+ await remove(targetPath, options, log);
75
+ } catch (err) {
76
+ log.error(`Error: ${err.message}`);
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // Member command
82
+ program
83
+ .command('member <name> [path]')
84
+ .description('Initialize a new member directory with personal dashboard')
85
+ .option('-f, --force', 'Force overwrite existing member directory')
86
+ .option('--no-config', 'Skip Obsidian daily-notes.json config update')
87
+ .action(async (name, targetPath = '.', options) => {
88
+ try {
89
+ await member(name, targetPath, options, log);
90
+ } catch (err) {
91
+ log.error(`Error: ${err.message}`);
92
+ process.exit(1);
93
+ }
94
+ });
95
+
96
+ // Completion command
97
+ program
98
+ .command('completion <shell>')
99
+ .description('Generate shell completion script (bash, zsh, fish)')
100
+ .action((shell) => {
101
+ const script = completion(shell);
102
+ if (script) {
103
+ // Ignore EPIPE errors (when pipe is closed early, e.g., piped to head)
104
+ process.stdout.on('error', (err) => {
105
+ if (err.code === 'EPIPE') {
106
+ process.exit(0);
107
+ }
108
+ throw err;
109
+ });
110
+ process.stdout.write(script);
111
+ } else {
112
+ log.error(`Unsupported shell: ${shell}. Use bash, zsh, or fish.`);
113
+ process.exit(1);
114
+ }
115
+ });
116
+
117
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@our2ndbrain/cli",
3
+ "version": "1.1.3",
4
+ "description": "CLI tool for 2ndBrain - A personal knowledge management system based on PARA + C-O-R-D + Append-and-Review",
5
+ "keywords": [
6
+ "2ndbrain",
7
+ "knowledge-management",
8
+ "obsidian",
9
+ "para",
10
+ "gtd",
11
+ "productivity"
12
+ ],
13
+ "author": "Our2ndBrain",
14
+ "license": "Apache-2.0",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Our2ndBrain/2ndBrain-Template.git"
18
+ },
19
+ "homepage": "https://github.com/Our2ndBrain/2ndBrain-Template",
20
+ "bin": {
21
+ "2ndbrain": "bin/2ndbrain.js"
22
+ },
23
+ "main": "./src/index.js",
24
+ "files": [
25
+ "bin/",
26
+ "src/",
27
+ ".obsidian/",
28
+ "00_Dashboard/",
29
+ "10_Inbox/Agents/",
30
+ "99_System/",
31
+ "AGENTS.md",
32
+ "CHANGELOG.md",
33
+ "CLAUDE.md",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "bugs": {
38
+ "url": "https://github.com/Our2ndBrain/2ndBrain-Template/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=16.0.0"
42
+ },
43
+ "scripts": {
44
+ "version": "node scripts/version.js && git add CHANGELOG.md",
45
+ "postversion": "git push && git push --tags && npm publish"
46
+ },
47
+ "dependencies": {
48
+ "chalk": "^4.1.2",
49
+ "commander": "^12.0.0",
50
+ "diff": "^5.2.0",
51
+ "fs-extra": "^11.2.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }
@@ -0,0 +1,198 @@
1
+ /**
2
+ * 2ndBrain CLI - Completion Command
3
+ *
4
+ * Generate shell completion scripts (kubectl style)
5
+ */
6
+
7
+ const BASH_COMPLETION = `# 2ndbrain bash completion
8
+ #
9
+ # Installation:
10
+ #
11
+ # ## Load completion into current shell
12
+ # source <(2ndbrain completion bash)
13
+ #
14
+ # ## Or write to file and source from .bash_profile (recommended for macOS)
15
+ # 2ndbrain completion bash > ~/.2ndbrain-completion.bash
16
+ # echo 'source ~/.2ndbrain-completion.bash' >> ~/.bash_profile
17
+ # source ~/.bash_profile
18
+ #
19
+ # Note: On macOS with bash 3.x, "source <(...)" may not work.
20
+ # Use the file-based method instead, or install bash 4+ via Homebrew.
21
+
22
+ _2ndbrain_completions() {
23
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
24
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
25
+ local cmd="\${COMP_WORDS[1]}"
26
+
27
+ # Top-level commands
28
+ local commands="init update remove member completion"
29
+
30
+ case "\$cmd" in
31
+ init)
32
+ case "\$prev" in
33
+ -t|--template)
34
+ COMPREPLY=( $(compgen -d -- "\$cur") )
35
+ return 0
36
+ ;;
37
+ esac
38
+ COMPREPLY=( $(compgen -W "-t --template -f --force -h --help" -- "\$cur") )
39
+ [[ -z "\$cur" || "\$cur" != -* ]] && COMPREPLY+=( $(compgen -d -- "\$cur") )
40
+ ;;
41
+ update)
42
+ case "\$prev" in
43
+ -t|--template)
44
+ COMPREPLY=( $(compgen -d -- "\$cur") )
45
+ return 0
46
+ ;;
47
+ esac
48
+ COMPREPLY=( $(compgen -W "-t --template -d --dry-run -h --help" -- "\$cur") )
49
+ [[ -z "\$cur" || "\$cur" != -* ]] && COMPREPLY+=( $(compgen -d -- "\$cur") )
50
+ ;;
51
+ remove)
52
+ COMPREPLY=( $(compgen -W "-d --dry-run -f --force -h --help" -- "\$cur") )
53
+ [[ -z "\$cur" || "\$cur" != -* ]] && COMPREPLY+=( $(compgen -d -- "\$cur") )
54
+ ;;
55
+ member)
56
+ COMPREPLY=( $(compgen -W "-f --force --no-config -h --help" -- "\$cur") )
57
+ ;;
58
+ completion)
59
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- "\$cur") )
60
+ ;;
61
+ *)
62
+ COMPREPLY=( $(compgen -W "\$commands -V --version -h --help" -- "\$cur") )
63
+ ;;
64
+ esac
65
+ }
66
+
67
+ complete -F _2ndbrain_completions 2ndbrain
68
+ `;
69
+
70
+ const ZSH_COMPLETION = `#compdef 2ndbrain
71
+
72
+ # 2ndbrain zsh completion
73
+
74
+ _2ndbrain() {
75
+ local -a commands
76
+ commands=(
77
+ 'init:Initialize a new 2ndBrain project'
78
+ 'update:Update framework files from template'
79
+ 'remove:Remove framework files (preserves user data)'
80
+ 'member:Initialize a new member directory'
81
+ 'completion:Generate shell completion script'
82
+ )
83
+
84
+ _arguments -C \\
85
+ '(-V --version)'{-V,--version}'[Show version]' \\
86
+ '(-h --help)'{-h,--help}'[Show help]' \\
87
+ '1: :->command' \\
88
+ '*:: :->args'
89
+
90
+ case \$state in
91
+ command)
92
+ _describe -t commands 'commands' commands
93
+ ;;
94
+ args)
95
+ case \$words[1] in
96
+ init)
97
+ _arguments \\
98
+ '(-t --template)'{-t,--template}'[Use custom template directory]:directory:_directories' \\
99
+ '(-f --force)'{-f,--force}'[Force overwrite existing project]' \\
100
+ '(-h --help)'{-h,--help}'[Show help]' \\
101
+ '1:path:_directories'
102
+ ;;
103
+ update)
104
+ _arguments \\
105
+ '(-t --template)'{-t,--template}'[Use custom template directory]:directory:_directories' \\
106
+ '(-d --dry-run)'{-d,--dry-run}'[Show what would be updated]' \\
107
+ '(-h --help)'{-h,--help}'[Show help]' \\
108
+ '1:path:_directories'
109
+ ;;
110
+ remove)
111
+ _arguments \\
112
+ '(-d --dry-run)'{-d,--dry-run}'[Show what would be removed]' \\
113
+ '(-f --force)'{-f,--force}'[Force removal without confirmation]' \\
114
+ '(-h --help)'{-h,--help}'[Show help]' \\
115
+ '1:path:_directories'
116
+ ;;
117
+ member)
118
+ _arguments \\
119
+ '(-f --force)'{-f,--force}'[Force overwrite existing member]' \\
120
+ '--no-config[Skip Obsidian config update]' \\
121
+ '(-h --help)'{-h,--help}'[Show help]' \\
122
+ '1:name:' \\
123
+ '2:path:_directories'
124
+ ;;
125
+ completion)
126
+ _arguments \\
127
+ '1:shell:(bash zsh fish)'
128
+ ;;
129
+ esac
130
+ ;;
131
+ esac
132
+ }
133
+
134
+ _2ndbrain
135
+ `;
136
+
137
+ const FISH_COMPLETION = `# 2ndbrain fish completion
138
+
139
+ # Disable file completion by default
140
+ complete -c 2ndbrain -f
141
+
142
+ # Top-level commands
143
+ complete -c 2ndbrain -n '__fish_use_subcommand' -a 'init' -d 'Initialize a new 2ndBrain project'
144
+ complete -c 2ndbrain -n '__fish_use_subcommand' -a 'update' -d 'Update framework files from template'
145
+ complete -c 2ndbrain -n '__fish_use_subcommand' -a 'remove' -d 'Remove framework files'
146
+ complete -c 2ndbrain -n '__fish_use_subcommand' -a 'member' -d 'Initialize a new member directory'
147
+ complete -c 2ndbrain -n '__fish_use_subcommand' -a 'completion' -d 'Generate shell completion script'
148
+
149
+ # Global options
150
+ complete -c 2ndbrain -n '__fish_use_subcommand' -s V -l version -d 'Show version'
151
+ complete -c 2ndbrain -n '__fish_use_subcommand' -s h -l help -d 'Show help'
152
+
153
+ # init options
154
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from init' -s t -l template -d 'Use custom template directory' -r -a '(__fish_complete_directories)'
155
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from init' -s f -l force -d 'Force overwrite existing project'
156
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from init' -s h -l help -d 'Show help'
157
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from init' -a '(__fish_complete_directories)'
158
+
159
+ # update options
160
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from update' -s t -l template -d 'Use custom template directory' -r -a '(__fish_complete_directories)'
161
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from update' -s d -l dry-run -d 'Show what would be updated'
162
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from update' -s h -l help -d 'Show help'
163
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from update' -a '(__fish_complete_directories)'
164
+
165
+ # remove options
166
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from remove' -s d -l dry-run -d 'Show what would be removed'
167
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from remove' -s f -l force -d 'Force removal without confirmation'
168
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from remove' -s h -l help -d 'Show help'
169
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from remove' -a '(__fish_complete_directories)'
170
+
171
+ # member options
172
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from member' -s f -l force -d 'Force overwrite existing member'
173
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from member' -l no-config -d 'Skip Obsidian config update'
174
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from member' -s h -l help -d 'Show help'
175
+
176
+ # completion options
177
+ complete -c 2ndbrain -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
178
+ `;
179
+
180
+ /**
181
+ * Generate shell completion script
182
+ * @param {string} shell - Shell type: bash, zsh, or fish
183
+ * @returns {string|null} Completion script or null if unsupported
184
+ */
185
+ function completion(shell) {
186
+ switch (shell) {
187
+ case 'bash':
188
+ return BASH_COMPLETION;
189
+ case 'zsh':
190
+ return ZSH_COMPLETION;
191
+ case 'fish':
192
+ return FISH_COMPLETION;
193
+ default:
194
+ return null;
195
+ }
196
+ }
197
+
198
+ module.exports = completion;
@@ -0,0 +1,308 @@
1
+ /**
2
+ * 2ndBrain CLI - Init Command
3
+ *
4
+ * Initialize a new 2ndBrain project or integrate into existing vault.
5
+ */
6
+
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const {
10
+ TEMPLATE_ROOT,
11
+ FRAMEWORK_FILES,
12
+ FRAMEWORK_DIRS,
13
+ USER_DATA_DIRS,
14
+ COPY_DIRS,
15
+ SMART_COPY_DIRS,
16
+ INIT_ONLY_FILES,
17
+ is2ndBrainProject,
18
+ } = require('../lib/config');
19
+ const { copyFiles, ensureDirs, createFile, isDirEmpty, copyFilesSmart } = require('../lib/files');
20
+ const { copyObsidianDirSmart } = require('../lib/obsidian');
21
+
22
+ /**
23
+ * Scan existing directory to understand current state
24
+ * @param {string} targetPath - Target directory path
25
+ * @returns {Promise<Object>} Scan result with existing files/dirs
26
+ */
27
+ async function scanExistingDirectory(targetPath) {
28
+ const scan = {
29
+ hasFiles: false,
30
+ existingDirs: new Set(),
31
+ existingFiles: new Set(),
32
+ hasObsidian: false,
33
+ hasUserDataDirs: new Set(),
34
+ hasFrameworkDirs: new Set(),
35
+ };
36
+
37
+ if (!await fs.pathExists(targetPath)) {
38
+ return scan;
39
+ }
40
+
41
+ const entries = await fs.readdir(targetPath, { withFileTypes: true });
42
+
43
+ for (const entry of entries) {
44
+ if (entry.isDirectory()) {
45
+ scan.existingDirs.add(entry.name);
46
+ scan.hasFiles = true;
47
+
48
+ if (entry.name === '.obsidian') {
49
+ scan.hasObsidian = true;
50
+ }
51
+ if (USER_DATA_DIRS.includes(entry.name)) {
52
+ scan.hasUserDataDirs.add(entry.name);
53
+ }
54
+ if (FRAMEWORK_DIRS.some(d => d.startsWith(entry.name))) {
55
+ scan.hasFrameworkDirs.add(entry.name);
56
+ }
57
+ } else if (entry.isFile()) {
58
+ scan.existingFiles.add(entry.name);
59
+ scan.hasFiles = true;
60
+ }
61
+ }
62
+
63
+ return scan;
64
+ }
65
+
66
+ /**
67
+ * Handle .obsidian directory reset from template
68
+ * @param {string} obsidianSrc - Template .obsidian path
69
+ * @param {string} obsidianDest - Target .obsidian path
70
+ * @param {Function} log - Logger function
71
+ * @param {boolean} skipConfirmation - Skip confirmation if true
72
+ * @returns {Promise<boolean>} True if reset performed, false if cancelled
73
+ */
74
+ async function handleObsidianReset(obsidianSrc, obsidianDest, log, skipConfirmation = false) {
75
+ const { confirm } = require('../lib/prompt');
76
+
77
+ // Check if template exists
78
+ if (!await fs.pathExists(obsidianSrc)) {
79
+ log.warn(' Template .obsidian not found, skipping reset');
80
+ return false;
81
+ }
82
+
83
+ const targetExists = await fs.pathExists(obsidianDest);
84
+
85
+ // If target doesn't exist, just copy (no confirmation needed)
86
+ if (!targetExists) {
87
+ log.info(' No existing .obsidian found, creating from template...');
88
+ await fs.copy(obsidianSrc, obsidianDest);
89
+ log.success(' + .obsidian/');
90
+ return true;
91
+ }
92
+
93
+ // Target exists - require confirmation
94
+ log.warn('');
95
+ log.warn('╔════════════════════════════════════════════════════════════╗');
96
+ log.warn('║ ⚠️ OBSIDIAN CONFIGURATION RESET WARNING ║');
97
+ log.warn('╚════════════════════════════════════════════════════════════╝');
98
+ log.warn('');
99
+ log.warn('You are about to COMPLETELY REPLACE your .obsidian directory.');
100
+ log.warn('This will delete all your Obsidian settings and preferences.');
101
+ log.warn('');
102
+
103
+ if (skipConfirmation) {
104
+ log.warn(' Force reset: skipping confirmation (--force)');
105
+ }
106
+
107
+ const confirmed = skipConfirmation || await confirm(
108
+ 'Are you sure you want to reset your .obsidian directory?',
109
+ false // Default to NO for safety
110
+ );
111
+
112
+ if (!confirmed) {
113
+ log.info('');
114
+ log.info('Obsidian reset cancelled. Preserving existing configuration.');
115
+ return false;
116
+ }
117
+
118
+ // Perform the reset
119
+ try {
120
+ log.info(' Resetting .obsidian directory...');
121
+ await fs.remove(obsidianDest);
122
+ await fs.copy(obsidianSrc, obsidianDest);
123
+ log.success(' .obsidian/ reset complete');
124
+ log.info(' • All settings replaced with template defaults');
125
+ return true;
126
+ } catch (err) {
127
+ log.error(` Failed to reset .obsidian: ${err.message}`);
128
+ throw err;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Initialize a new 2ndBrain project or integrate into existing vault
134
+ * @param {string} targetPath - Target directory path
135
+ * @param {Object} options - Command options
136
+ * @param {string} [options.template] - Custom template path
137
+ * @param {boolean} [options.force] - Force overwrite existing 2ndBrain project
138
+ * @param {boolean} [options.resetObsidian] - Reset .obsidian from template
139
+ * @param {Function} log - Logger function
140
+ */
141
+ async function init(targetPath, options, log) {
142
+ const resolvedPath = path.resolve(targetPath);
143
+ const templateRoot = options.template ? path.resolve(options.template) : TEMPLATE_ROOT;
144
+
145
+ log.info(`Initializing 2ndBrain project at: ${resolvedPath}`);
146
+ log.info(`Using template from: ${templateRoot}`);
147
+
148
+ // Check if target directory exists and scan it
149
+ const dirExists = await fs.pathExists(resolvedPath);
150
+ const isEmpty = dirExists && await isDirEmpty(resolvedPath);
151
+ const isExistingProject = dirExists && is2ndBrainProject(resolvedPath);
152
+
153
+ // Handle existing 2ndBrain project
154
+ if (isExistingProject) {
155
+ if (!options.force) {
156
+ log.warn('This directory is already a 2ndBrain project.');
157
+ log.info('Use "2ndbrain update" to update framework files.');
158
+ log.info('Or use --force to reinitialize (this will overwrite framework files).');
159
+ throw new Error('Target directory is already a 2ndBrain project');
160
+ }
161
+ log.warn('Reinitializing existing 2ndBrain project (--force)...');
162
+ }
163
+
164
+ // Create target directory if needed
165
+ await fs.ensureDir(resolvedPath);
166
+
167
+ // Scan existing directory for smart integration
168
+ const scan = await scanExistingDirectory(resolvedPath);
169
+ const isIntegrateMode = !isEmpty && scan.hasFiles && !isExistingProject;
170
+
171
+ if (isIntegrateMode) {
172
+ log.info('');
173
+ log.info('Integration mode: Merging 2ndBrain framework into existing vault');
174
+ if (scan.hasUserDataDirs.size > 0) {
175
+ log.info(`Found existing user data: ${[...scan.hasUserDataDirs].join(', ')}`);
176
+ }
177
+ if (scan.hasFrameworkDirs.size > 0) {
178
+ log.warn(`Found existing framework dirs: ${[...scan.hasFrameworkDirs].join(', ')}`);
179
+ }
180
+ log.info('');
181
+ }
182
+
183
+ // Step 1: Create framework directories (only missing ones)
184
+ log.info('Ensuring directories exist...');
185
+ const dirsToCreate = [];
186
+
187
+ for (const dir of FRAMEWORK_DIRS) {
188
+ const dirPath = path.join(resolvedPath, dir);
189
+ if (!await fs.pathExists(dirPath)) {
190
+ dirsToCreate.push(dir);
191
+ }
192
+ }
193
+
194
+ // Create missing framework directories
195
+ if (dirsToCreate.length > 0) {
196
+ await ensureDirs(dirsToCreate, resolvedPath);
197
+ dirsToCreate.forEach(dir => log.success(` + ${dir}/`));
198
+ } else {
199
+ log.info(' All framework directories exist');
200
+ }
201
+
202
+ // Ensure user data directories exist (never overwrite)
203
+ const userDataDirsToCreate = [];
204
+ for (const dir of USER_DATA_DIRS) {
205
+ const dirPath = path.join(resolvedPath, dir);
206
+ if (!await fs.pathExists(dirPath)) {
207
+ userDataDirsToCreate.push(dir);
208
+ }
209
+ }
210
+
211
+ if (userDataDirsToCreate.length > 0) {
212
+ await ensureDirs(userDataDirsToCreate, resolvedPath);
213
+ userDataDirsToCreate.forEach(dir => log.success(` + ${dir}/ (user data)`));
214
+ }
215
+
216
+ // Step 2: Copy framework files (skip existing)
217
+ log.info('Copying framework files...');
218
+ const fileResult = await copyFilesSmart(
219
+ FRAMEWORK_FILES,
220
+ templateRoot,
221
+ resolvedPath,
222
+ {},
223
+ (file, action, detail) => {
224
+ if (action === 'copy') {
225
+ log.success(` + ${file}`);
226
+ } else if (action === 'unchanged') {
227
+ log.info(` = ${file} (exists, unchanged)`);
228
+ } else if (action === 'error') {
229
+ log.error(` ! ${file} (error: ${detail})`);
230
+ }
231
+ }
232
+ );
233
+
234
+ // Step 3: Process .obsidian directory
235
+ log.info('Processing .obsidian configuration...');
236
+ const obsidianSrc = path.join(templateRoot, '.obsidian');
237
+ const obsidianDest = path.join(resolvedPath, '.obsidian');
238
+
239
+ if (await fs.pathExists(obsidianSrc)) {
240
+ // Handle --reset-obsidian option
241
+ if (options.resetObsidian) {
242
+ await handleObsidianReset(obsidianSrc, obsidianDest, log, options.force);
243
+ } else if (await fs.pathExists(obsidianDest)) {
244
+ // Smart merge existing .obsidian
245
+ log.info(' Merging .obsidian/ (preserving your settings)...');
246
+ const obsidianResult = await copyObsidianDirSmart(
247
+ obsidianSrc,
248
+ obsidianDest,
249
+ {
250
+ onProgress: (file, action, detail) => {
251
+ if (action === 'added') {
252
+ log.success(` + ${file}`);
253
+ } else if (action === 'merged') {
254
+ log.success(` * ${file} (merged)`);
255
+ if (detail.added && detail.added.length > 0) {
256
+ log.info(` plugins added: ${detail.added.join(', ')}`);
257
+ }
258
+ } else if (action === 'preserved') {
259
+ log.info(` = ${file} (preserved)`);
260
+ }
261
+ },
262
+ }
263
+ );
264
+ log.success(` .obsidian/ merged: ${obsidianResult.added.length + obsidianResult.merged.length} updated`);
265
+ } else {
266
+ // New .obsidian directory
267
+ await fs.copy(obsidianSrc, obsidianDest);
268
+ log.success(` + .obsidian/`);
269
+ }
270
+ }
271
+
272
+ // Step 4: Create init-only files (if missing)
273
+ log.info('Creating initial files...');
274
+ for (const { path: filePath, content } of INIT_ONLY_FILES) {
275
+ const fullPath = path.join(resolvedPath, filePath);
276
+ if (!await fs.pathExists(fullPath)) {
277
+ await createFile(fullPath, content);
278
+ log.success(` + ${filePath}`);
279
+ }
280
+ }
281
+
282
+ // Step 5: Create .gitkeep files for user data directories
283
+ for (const dir of USER_DATA_DIRS) {
284
+ const gitkeepPath = path.join(resolvedPath, dir, '.gitkeep');
285
+ if (!await fs.pathExists(gitkeepPath)) {
286
+ await createFile(gitkeepPath, '');
287
+ }
288
+ }
289
+
290
+ // Summary
291
+ log.info('');
292
+ log.success(isIntegrateMode ? '2ndBrain framework integrated!' : '2ndBrain project initialized!');
293
+ log.info(` Created: ${fileResult.copied.length} files`);
294
+ if (fileResult.unchanged.length > 0) {
295
+ log.info(` Skipped: ${fileResult.unchanged.length} existing files`);
296
+ }
297
+ if (fileResult.errors.length > 0) {
298
+ log.error(` Errors: ${fileResult.errors.length} files`);
299
+ }
300
+
301
+ log.info('');
302
+ log.info('Next steps:');
303
+ log.info(' 1. Open this directory with Obsidian');
304
+ log.info(' 2. Run: ./99_System/Scripts/init_member.sh <your-name>');
305
+ log.info(' 3. Start recording your first task!');
306
+ }
307
+
308
+ module.exports = init;