@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
package/bin/2ndbrain.js
ADDED
|
@@ -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;
|