create-vault-cms 1.1.0 → 1.1.1

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +258 -204
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vault-cms",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Installer for Vault CMS",
5
5
  "main": "src/cli.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -1,204 +1,258 @@
1
- #!/usr/bin/env node
2
-
3
- const { Command } = require('commander');
4
- const fs = require('fs-extra');
5
- const path = require('path');
6
- const https = require('https');
7
- const AdmZip = require('adm-zip');
8
- const inquirer = require('inquirer');
9
-
10
- const pkg = require('../package.json');
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name('create-vault-cms')
16
- .description('Official installer for Vault CMS')
17
- .version(pkg.version);
18
-
19
- program
20
- .argument('[target]', 'target directory')
21
- .option('-t, --template <name>', 'template to use (from vault-cms-presets)')
22
- .action(async (target, options) => {
23
- try {
24
- console.log('šŸš€ Initializing Vault CMS Installer...');
25
-
26
- const availableTemplates = await fetchTemplates();
27
-
28
- let template = options.template;
29
- let targetPath = target;
30
-
31
- if (targetPath && availableTemplates.includes(targetPath.toLowerCase()) && !template) {
32
- template = targetPath.toLowerCase();
33
- targetPath = null;
34
- }
35
-
36
- if (!template) {
37
- const { useTemplate } = await inquirer.prompt([{
38
- type: 'confirm',
39
- name: 'useTemplate',
40
- message: 'Would you like to use a preset template (e.g. Starlight, Slate)?',
41
- default: false
42
- }]);
43
-
44
- if (useTemplate) {
45
- const { selectedTemplate } = await inquirer.prompt([{
46
- type: 'list',
47
- name: 'selectedTemplate',
48
- message: 'Select a template:',
49
- choices: availableTemplates
50
- }]);
51
- template = selectedTemplate;
52
- }
53
- }
54
-
55
- if (!targetPath) {
56
- const answers = await inquirer.prompt([
57
- {
58
- type: 'input',
59
- name: 'path',
60
- message: 'Where should we install Vault CMS?',
61
- default: 'src/content',
62
- }
63
- ]);
64
- targetPath = answers.path;
65
- }
66
-
67
- const targetDir = path.resolve(targetPath);
68
- const tempZip = path.join(targetDir, 'vault-cms-temp.zip');
69
- const extractDir = path.join(targetDir, '.vault-cms-temp-extract');
70
-
71
- const repoName = template ? 'vault-cms-presets' : 'vault-cms';
72
- const zipUrl = `https://github.com/davidvkimball/${repoName}/archive/refs/heads/master.zip`;
73
-
74
- console.log(`\nšŸš€ Installing Vault CMS${template ? ` (template: ${template})` : ''}...`);
75
- console.log(` šŸ“ Target directory: ${targetDir}`);
76
-
77
- await fs.ensureDir(targetDir);
78
-
79
- console.log(' šŸ“¦ Downloading archive...');
80
- await downloadFile(zipUrl, tempZip);
81
-
82
- console.log(' šŸ“‚ Extracting files...');
83
- const zip = new AdmZip(tempZip);
84
- zip.extractAllTo(extractDir, true);
85
-
86
- const items = await fs.readdir(extractDir);
87
- const folders = items.filter(item => fs.statSync(path.join(extractDir, item)).isDirectory());
88
-
89
- if (folders.length === 0) {
90
- throw new Error('Could not find content in the downloaded archive.');
91
- }
92
-
93
- const innerFolder = path.join(extractDir, folders[0]);
94
- const sourcePath = template ? path.join(innerFolder, template) : innerFolder;
95
-
96
- if (!(await fs.pathExists(sourcePath))) {
97
- throw new Error(`Template "${template}" not found in presets repository.`);
98
- }
99
-
100
- const toKeep = ['_bases', '.obsidian', '_GUIDE.md'];
101
- for (const item of toKeep) {
102
- const src = path.join(sourcePath, item);
103
- const dest = path.join(targetDir, item);
104
-
105
- if (await fs.pathExists(src)) {
106
- await fs.copy(src, dest, { overwrite: true });
107
- console.log(` āœ“ Added ${item}`);
108
- }
109
- }
110
-
111
- // Smart .gitignore logic: Look for project root
112
- const projectRoot = await findProjectRoot(targetDir);
113
- const gitignorePath = path.join(projectRoot, '.gitignore');
114
- const ignores = '\n# Vault CMS / Obsidian\n.obsidian/workspace.json\n.obsidian/workspace-mobile.json\n.ref/\n';
115
-
116
- const isExternalRoot = projectRoot !== targetDir && !targetDir.startsWith(projectRoot);
117
-
118
- if (await fs.pathExists(gitignorePath)) {
119
- const content = await fs.readFile(gitignorePath, 'utf8');
120
- if (!content.includes('.obsidian/workspace.json')) {
121
- await fs.appendFile(gitignorePath, ignores);
122
- console.log(` āœ“ Updated .gitignore at ${path.relative(process.cwd(), gitignorePath)}`);
123
- }
124
- } else if (!isExternalRoot) {
125
- await fs.writeFile(gitignorePath, ignores.trim() + '\n');
126
- console.log(` āœ“ Created .gitignore at ${path.relative(process.cwd(), gitignorePath)}`);
127
- } else {
128
- console.log(` āš ļø Skipped .gitignore (could not find a safe project root)`);
129
- }
130
-
131
- await fs.remove(tempZip);
132
- await fs.remove(extractDir);
133
-
134
- if (projectRoot === targetDir) {
135
- console.log('\n āš ļø Note: No Astro project or package.json found in parent directories.');
136
- console.log(' Installation completed, but you may need to move these files into your content folder manually.');
137
- }
138
-
139
- console.log('\n✨ Vault CMS is ready!');
140
- process.exit(0);
141
- } catch (err) {
142
- console.error('\nāŒ Installation failed:', err.message);
143
- process.exit(1);
144
- }
145
- });
146
-
147
- async function findProjectRoot(startDir) {
148
- let current = startDir;
149
- // Look up to 6 levels up for a project root (Astro config, package.json, or .git)
150
- let depth = 0;
151
- while (current !== path.parse(current).root && depth < 6) {
152
- const hasPkg = await fs.pathExists(path.join(current, 'package.json'));
153
- const hasAstro = await fs.pathExists(path.join(current, 'astro.config.mjs')) || await fs.pathExists(path.join(current, 'astro.config.ts'));
154
- const hasGit = await fs.pathExists(path.join(current, '.git'));
155
-
156
- if (hasPkg || hasAstro || hasGit) return current;
157
-
158
- current = path.dirname(current);
159
- depth++;
160
- }
161
- return startDir; // Fallback to target dir
162
- }
163
-
164
- function downloadFile(url, dest) {
165
- return new Promise((resolve, reject) => {
166
- https.get(url, { headers: { 'User-Agent': 'vault-cms-installer' } }, (res) => {
167
- if (res.statusCode === 301 || res.statusCode === 302) {
168
- return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
169
- }
170
- if (res.statusCode !== 200) {
171
- return reject(new Error(`Failed to download: ${res.statusCode}`));
172
- }
173
- const file = fs.createWriteStream(dest);
174
- res.pipe(file);
175
- file.on('finish', () => {
176
- file.close();
177
- resolve();
178
- });
179
- }).on('error', reject);
180
- });
181
- }
182
-
183
- function fetchTemplates() {
184
- return new Promise((resolve) => {
185
- const url = 'https://api.github.com/repos/davidvkimball/vault-cms-presets/contents';
186
- https.get(url, { headers: { 'User-Agent': 'vault-cms-installer' } }, (res) => {
187
- let data = '';
188
- res.on('data', (chunk) => data += chunk);
189
- res.on('end', () => {
190
- try {
191
- const contents = JSON.parse(data);
192
- const dirs = contents
193
- .filter(item => item.type === 'dir' && !item.name.startsWith('.'))
194
- .map(item => item.name);
195
- resolve(dirs);
196
- } catch (e) {
197
- resolve(['starlight', 'slate', 'chiri']);
198
- }
199
- });
200
- }).on('error', () => resolve(['starlight', 'slate', 'chiri']));
201
- });
202
- }
203
-
204
- program.parse();
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const fs = require('fs-extra');
5
+ const path = require('path');
6
+ const https = require('https');
7
+ const AdmZip = require('adm-zip');
8
+ const inquirer = require('inquirer');
9
+ const { exec } = require('child_process');
10
+
11
+ const pkg = require('../package.json');
12
+
13
+ const program = new Command();
14
+
15
+ program
16
+ .name('create-vault-cms')
17
+ .description('Official installer for Vault CMS')
18
+ .version(pkg.version);
19
+
20
+ program
21
+ .argument('[target]', 'target directory')
22
+ .option('-t, --template <name>', 'template to use (from vault-cms-presets)')
23
+ .action(async (target, options) => {
24
+ try {
25
+ console.log('šŸš€ Initializing Vault CMS Installer...');
26
+
27
+ const availableTemplates = await fetchTemplates();
28
+
29
+ let template = options.template;
30
+ let targetPath = target;
31
+
32
+ if (targetPath && availableTemplates.includes(targetPath.toLowerCase()) && !template) {
33
+ template = targetPath.toLowerCase();
34
+ targetPath = null;
35
+ }
36
+
37
+ if (!template) {
38
+ const { useTemplate } = await inquirer.prompt([{
39
+ type: 'confirm',
40
+ name: 'useTemplate',
41
+ message: 'Would you like to use a preset template (e.g. Starlight, Slate)?',
42
+ default: false
43
+ }]);
44
+
45
+ if (useTemplate) {
46
+ const { selectedTemplate } = await inquirer.prompt([{
47
+ type: 'list',
48
+ name: 'selectedTemplate',
49
+ message: 'Select a template:',
50
+ choices: availableTemplates
51
+ }]);
52
+ template = selectedTemplate;
53
+ }
54
+ }
55
+
56
+ if (!targetPath) {
57
+ const answers = await inquirer.prompt([
58
+ {
59
+ type: 'input',
60
+ name: 'path',
61
+ message: 'Where should we install Vault CMS?',
62
+ default: 'src/content',
63
+ }
64
+ ]);
65
+ targetPath = answers.path;
66
+ }
67
+
68
+ const targetDir = path.resolve(targetPath);
69
+ const tempZip = path.join(targetDir, 'vault-cms-temp.zip');
70
+ const extractDir = path.join(targetDir, '.vault-cms-temp-extract');
71
+
72
+ const repoName = template ? 'vault-cms-presets' : 'vault-cms';
73
+ const zipUrl = `https://github.com/davidvkimball/${repoName}/archive/refs/heads/master.zip`;
74
+
75
+ console.log(`\nšŸš€ Installing Vault CMS${template ? ` (template: ${template})` : ''}...`);
76
+ console.log(` šŸ“ Target directory: ${targetDir}`);
77
+
78
+ await fs.ensureDir(targetDir);
79
+
80
+ console.log(' šŸ“¦ Downloading archive...');
81
+ await downloadFile(zipUrl, tempZip);
82
+
83
+ console.log(' šŸ“‚ Extracting files...');
84
+ const zip = new AdmZip(tempZip);
85
+ zip.extractAllTo(extractDir, true);
86
+
87
+ const items = await fs.readdir(extractDir);
88
+ const folders = items.filter(item => fs.statSync(path.join(extractDir, item)).isDirectory());
89
+
90
+ if (folders.length === 0) {
91
+ throw new Error('Could not find content in the downloaded archive.');
92
+ }
93
+
94
+ const innerFolder = path.join(extractDir, folders[0]);
95
+ const sourcePath = template ? path.join(innerFolder, template) : innerFolder;
96
+
97
+ if (!(await fs.pathExists(sourcePath))) {
98
+ throw new Error(`Template "${template}" not found in presets repository.`);
99
+ }
100
+
101
+ const toKeep = ['_bases', '.obsidian', '_GUIDE.md'];
102
+ for (const item of toKeep) {
103
+ const src = path.join(sourcePath, item);
104
+ const dest = path.join(targetDir, item);
105
+
106
+ if (await fs.pathExists(src)) {
107
+ await fs.copy(src, dest, { overwrite: true });
108
+ console.log(` āœ“ Added ${item}`);
109
+ }
110
+ }
111
+
112
+ // Smart .gitignore logic: Look for project root
113
+ const projectRoot = await findProjectRoot(targetDir);
114
+ const gitignorePath = path.join(projectRoot, '.gitignore');
115
+ const ignores = '\n# Vault CMS / Obsidian\n.obsidian/workspace.json\n.obsidian/workspace-mobile.json\n.ref/\n';
116
+
117
+ const isExternalRoot = projectRoot !== targetDir && !targetDir.startsWith(projectRoot);
118
+
119
+ if (await fs.pathExists(gitignorePath)) {
120
+ const content = await fs.readFile(gitignorePath, 'utf8');
121
+ if (!content.includes('.obsidian/workspace.json')) {
122
+ await fs.appendFile(gitignorePath, ignores);
123
+ console.log(` āœ“ Updated .gitignore at ${path.relative(process.cwd(), gitignorePath)}`);
124
+ }
125
+ } else if (!isExternalRoot) {
126
+ await fs.writeFile(gitignorePath, ignores.trim() + '\n');
127
+ console.log(` āœ“ Created .gitignore at ${path.relative(process.cwd(), gitignorePath)}`);
128
+ } else {
129
+ console.log(` āš ļø Skipped .gitignore (could not find a safe project root)`);
130
+ }
131
+
132
+ await fs.remove(tempZip);
133
+ await fs.remove(extractDir);
134
+
135
+ if (projectRoot === targetDir) {
136
+ console.log('\n āš ļø Note: No Astro project or package.json found in parent directories.');
137
+ console.log(' Installation completed, but you may need to move these files into your content folder manually.');
138
+ }
139
+
140
+ console.log('\n✨ Vault CMS is ready!');
141
+
142
+ const { openObsidian } = await inquirer.prompt([{
143
+ type: 'confirm',
144
+ name: 'openObsidian',
145
+ message: 'Would you like to open this folder in Obsidian now?',
146
+ default: true
147
+ }]);
148
+
149
+ if (openObsidian) {
150
+ await openInObsidian(targetDir);
151
+ }
152
+
153
+ process.exit(0);
154
+ } catch (err) {
155
+ console.error('\nāŒ Installation failed:', err.message);
156
+ process.exit(1);
157
+ }
158
+ });
159
+
160
+ async function openInObsidian(targetPath) {
161
+ // Obsidian URIs require forward slashes
162
+ const normalizedPath = targetPath.replace(/\\/g, '/');
163
+
164
+ // Adding a trailing slash often helps Obsidian recognize it as a folder/vault
165
+ const folderUri = `obsidian://open?path=${encodeURIComponent(normalizedPath + '/')}`;
166
+
167
+ const anchors = [
168
+ path.join('_bases', 'Home.base'),
169
+ '_GUIDE.md'
170
+ ];
171
+
172
+ let anchorFile = '';
173
+ for (const a of anchors) {
174
+ if (await fs.pathExists(path.join(targetPath, a))) {
175
+ anchorFile = a;
176
+ break;
177
+ }
178
+ }
179
+
180
+ const fileUri = anchorFile
181
+ ? `obsidian://open?path=${encodeURIComponent(normalizedPath + '/' + anchorFile.replace(/\\/g, '/'))}`
182
+ : folderUri;
183
+
184
+ return new Promise((resolve) => {
185
+ const command = process.platform === 'win32'
186
+ ? `start "" "${fileUri}"`
187
+ : process.platform === 'darwin'
188
+ ? `open "${fileUri}"`
189
+ : `xdg-open "${fileUri}"`;
190
+
191
+ console.log(` šŸ“‚ Opening Obsidian: ${fileUri}`);
192
+
193
+ exec(command, (error) => {
194
+ if (error) {
195
+ console.error(` āŒ Failed to open Obsidian: ${error.message}`);
196
+ }
197
+ resolve();
198
+ });
199
+ });
200
+ }
201
+ async function findProjectRoot(startDir) {
202
+ let current = startDir;
203
+ // Look up to 6 levels up for a project root (Astro config, package.json, or .git)
204
+ let depth = 0;
205
+ while (current !== path.parse(current).root && depth < 6) {
206
+ const hasPkg = await fs.pathExists(path.join(current, 'package.json'));
207
+ const hasAstro = await fs.pathExists(path.join(current, 'astro.config.mjs')) || await fs.pathExists(path.join(current, 'astro.config.ts'));
208
+ const hasGit = await fs.pathExists(path.join(current, '.git'));
209
+
210
+ if (hasPkg || hasAstro || hasGit) return current;
211
+
212
+ current = path.dirname(current);
213
+ depth++;
214
+ }
215
+ return startDir; // Fallback to target dir
216
+ }
217
+
218
+ function downloadFile(url, dest) {
219
+ return new Promise((resolve, reject) => {
220
+ https.get(url, { headers: { 'User-Agent': 'vault-cms-installer' } }, (res) => {
221
+ if (res.statusCode === 301 || res.statusCode === 302) {
222
+ return downloadFile(res.headers.location, dest).then(resolve).catch(reject);
223
+ }
224
+ if (res.statusCode !== 200) {
225
+ return reject(new Error(`Failed to download: ${res.statusCode}`));
226
+ }
227
+ const file = fs.createWriteStream(dest);
228
+ res.pipe(file);
229
+ file.on('finish', () => {
230
+ file.close();
231
+ resolve();
232
+ });
233
+ }).on('error', reject);
234
+ });
235
+ }
236
+
237
+ function fetchTemplates() {
238
+ return new Promise((resolve) => {
239
+ const url = 'https://api.github.com/repos/davidvkimball/vault-cms-presets/contents';
240
+ https.get(url, { headers: { 'User-Agent': 'vault-cms-installer' } }, (res) => {
241
+ let data = '';
242
+ res.on('data', (chunk) => data += chunk);
243
+ res.on('end', () => {
244
+ try {
245
+ const contents = JSON.parse(data);
246
+ const dirs = contents
247
+ .filter(item => item.type === 'dir' && !item.name.startsWith('.'))
248
+ .map(item => item.name);
249
+ resolve(dirs);
250
+ } catch (e) {
251
+ resolve(['starlight', 'slate', 'chiri']);
252
+ }
253
+ });
254
+ }).on('error', () => resolve(['starlight', 'slate', 'chiri']));
255
+ });
256
+ }
257
+
258
+ program.parse();