@chriswiegman/hugo-tools 0.1.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
6
+ and is generated by [Changie](https://github.com/miniscruff/changie).
7
+
8
+ ## 0.1.0 - 2026-05-04
9
+
10
+ ### Features
11
+
12
+ * Initial test release
package/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Chris Wiegman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # hugo-tools
2
+
3
+ A collection of CLI tools for Hugo bloggers. Run them directly from the root of your Hugo site via `npx` or install globally.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18 or later
8
+
9
+ ## Installation
10
+
11
+ **Run without installing (recommended):**
12
+
13
+ ```sh
14
+ npx -p @chriswiegman/hugo-tools <command> [args]
15
+ ```
16
+
17
+ For example:
18
+
19
+ ```sh
20
+ npx -p @chriswiegman/hugo-tools draft
21
+ npx -p @chriswiegman/hugo-tools publish content/drafts/my-post.md now
22
+ ```
23
+
24
+ **Install locally in your Hugo project:**
25
+
26
+ ```sh
27
+ npm install --save-dev @chriswiegman/hugo-tools
28
+ ```
29
+
30
+ Then run commands via `npx` (no `-p` flag needed once installed) or add them as scripts in your `package.json`:
31
+
32
+ ```json
33
+ {
34
+ "scripts": {
35
+ "publish": "publish",
36
+ "extract-tags": "extract-tags"
37
+ }
38
+ }
39
+ ```
40
+
41
+ **Or install globally and run from any Hugo site root:**
42
+
43
+ ```sh
44
+ npm install -g @chriswiegman/hugo-tools
45
+ publish content/drafts/my-post.md now
46
+ ```
47
+
48
+ All commands must be run from the **root of your Hugo site**.
49
+
50
+ ---
51
+
52
+ ## Configuration
53
+
54
+ Run `config` from your Hugo site root to generate the file with all defaults:
55
+
56
+ ```sh
57
+ config
58
+ ```
59
+
60
+ Or create `.hugo-tools.json` manually:
61
+
62
+ ```json
63
+ {
64
+ "timezone": "America/Chicago",
65
+ "postsDir": "content/posts",
66
+ "draftsDir": "content/drafts",
67
+ "notesDir": "content/notes",
68
+ "booksDir": "content/books",
69
+ "imagesDir": "assets/images",
70
+ "vscodeDir": ".vscode"
71
+ }
72
+ ```
73
+
74
+ All fields are optional. Shown values are the defaults.
75
+
76
+ ---
77
+
78
+ ## Commands
79
+
80
+ ### `config`
81
+
82
+ Generates a `.hugo-tools.json` config file in the current directory with all default values.
83
+
84
+ ```sh
85
+ config
86
+ ```
87
+
88
+ Run this once after installing to get a config file you can edit. Exits with an error if `.hugo-tools.json` already exists.
89
+
90
+ ---
91
+
92
+ ### `vscode`
93
+
94
+ Scaffolds VS Code configuration for your Hugo site in your `vscodeDir` (default `.vscode/`).
95
+
96
+ ```sh
97
+ vscode
98
+ ```
99
+
100
+ Creates three files if they don't already exist — existing files are left untouched:
101
+
102
+ | File | Purpose |
103
+ |---|---|
104
+ | `tasks.json` | Tasks for `draft`, `add-tags`, `publish` (now & later), and `pick-image` |
105
+ | `extensions.json` | Recommends the markdown word-count and spell-checker extensions |
106
+ | `settings.json` | Markdown editor settings (snippet suggestions, word-based suggestions off) |
107
+
108
+ ---
109
+
110
+ ### `draft`
111
+
112
+ Creates a new draft file in your `draftsDir` and opens it in VS Code.
113
+
114
+ ```sh
115
+ draft
116
+ ```
117
+
118
+ Generates a timestamp-based filename (`YYYYMMDD-HHMMSS.md`) using your configured timezone, writes the standard draft front matter with a blank title and placeholder lists, and prints the path. No `date:` line is written — fill in the title before publishing.
119
+
120
+ **Generated front matter:**
121
+
122
+ ```yaml
123
+ ---
124
+ title: ""
125
+ description: ""
126
+ draft: true
127
+ images:
128
+ -
129
+ categories:
130
+ -
131
+ tags:
132
+ -
133
+ ---
134
+ ```
135
+
136
+ ---
137
+
138
+ ### `publish`
139
+
140
+ Publishes a Hugo draft post by moving it from your drafts directory to the appropriate content directory and updating its front matter.
141
+
142
+ ```sh
143
+ publish <draft-path> [now|later] [YYYY-MM-DD]
144
+ ```
145
+
146
+ **Examples:**
147
+
148
+ ```sh
149
+ # Publish immediately
150
+ publish content/drafts/my-post.md now
151
+
152
+ # Schedule for a future date (prompts if date omitted)
153
+ publish content/drafts/my-post.md later 2026-06-01
154
+ ```
155
+
156
+ **What it does:**
157
+
158
+ - Sets `draft: false` and `date:` to now or 08:00 on the scheduled date (in your configured timezone)
159
+ - Moves the file to `content/posts/YYYY/MM-DD-slug.md`
160
+ - Short posts (under 200 words) without categories are treated as notes and moved to `content/notes/` instead
161
+ - Warns if the draft already had a different `date:` value
162
+
163
+ **Requirements for posts** (not notes):
164
+
165
+ - A `title:` in front matter
166
+ - At least one tag
167
+
168
+ ---
169
+
170
+ ### `extract-tags`
171
+
172
+ Scans all posts in your `postsDir` and writes two files to your `vscodeDir`:
173
+
174
+ - `tags-categories.json` — sorted list of all tags and categories with usage counts
175
+ - `markdown.code-snippets` — VSCode snippet definitions for autocomplete
176
+
177
+ ```sh
178
+ extract-tags
179
+ ```
180
+
181
+ Run this periodically to keep your tag/category list fresh. The snippet abbreviation map in [src/extract-tags.js](src/extract-tags.js) is pre-populated with some common shorthands — edit it to match your own taxonomy.
182
+
183
+ ---
184
+
185
+ ### `add-tags`
186
+
187
+ Interactive prompt to select categories and tags from your existing taxonomy, then prints the formatted front matter block to paste into a draft.
188
+
189
+ ```sh
190
+ add-tags
191
+ ```
192
+
193
+ Requires `extract-tags` to have been run at least once.
194
+
195
+ ---
196
+
197
+ ### `pick-image`
198
+
199
+ Lists images in your `imagesDir` for the current year/month, lets you pick one, and copies the Hugo-relative path to the clipboard.
200
+
201
+ ```sh
202
+ pick-image
203
+ ```
204
+
205
+ Clipboard support: macOS (`pbcopy`), Windows (`clip`), Linux (`xclip`, `xsel`, or `wl-copy` — whichever is available).
206
+
207
+ ---
208
+
209
+ ### `import-books`
210
+
211
+ Imports books from a [Goodreads CSV export](https://www.goodreads.com/review/import) into Hugo content files under `booksDir`.
212
+
213
+ ```sh
214
+ import-books path/to/goodreads_library_export.csv
215
+ ```
216
+
217
+ **What it does:**
218
+
219
+ - Creates `content/books/<author-slug>/<title-slug>.md` for each book on your "read" shelf
220
+ - Front matter includes title, author, star rating, finish date(s), and links to Amazon, Open Library, and Goodreads
221
+ - Any Goodreads review text becomes the file body
222
+ - Safe to re-run: existing files are matched by Goodreads ID or ISBN, finish dates are merged, and unchanged files are skipped
223
+ - If a book's title changed in Goodreads, the file is renamed automatically
224
+
225
+ **To export from Goodreads:** Account → My Books → Import and Export → Export Library.
226
+
227
+ ---
228
+
229
+ ## Contributing
230
+
231
+ Issues and PRs welcome at <https://github.com/ChrisWiegman/hugo-tools>.
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@chriswiegman/hugo-tools",
3
+ "version": "0.1.0",
4
+ "description": "A set of tools to make blogging easier with Hugo.",
5
+ "keywords": [
6
+ "hugo",
7
+ "blogging",
8
+ "drafts",
9
+ "publishing"
10
+ ],
11
+ "homepage": "https://github.com/ChrisWiegman/hugo-tools#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/ChrisWiegman/hugo-tools/issues"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/ChrisWiegman/hugo-tools.git"
18
+ },
19
+ "license": "MIT",
20
+ "author": "Chris Wiegman <contact@chriswiegman.com>",
21
+ "type": "commonjs",
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "files": [
26
+ "src/",
27
+ "CHANGELOG.md"
28
+ ],
29
+ "bin": {
30
+ "config": "src/config-init.js",
31
+ "vscode": "src/vscode.js",
32
+ "extract-tags": "src/extract-tags.js",
33
+ "add-tags": "src/add-tags.js",
34
+ "draft": "src/draft.js",
35
+ "pick-image": "src/pick-image.js",
36
+ "publish": "src/publish.js",
37
+ "import-books": "src/book.mjs"
38
+ },
39
+ "scripts": {
40
+ "lint": "eslint src/",
41
+ "test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs"
42
+ },
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "changie": "^1.24.0",
46
+ "eslint": "^10.3.0",
47
+ "globals": "^17.6.0"
48
+ }
49
+ }
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Interactive script to help add categories and tags to Hugo draft frontmatter.
5
+ * Reads from the extracted tags-categories.json file.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const readline = require('readline');
11
+ const config = require('./config');
12
+
13
+ const DATA_FILE = path.join(process.cwd(), config.vscodeDir, 'tags-categories.json');
14
+
15
+ /**
16
+ * Display menu and get user selection
17
+ */
18
+ async function getSelection(prompt, options) {
19
+ const rl = readline.createInterface({
20
+ input: process.stdin,
21
+ output: process.stdout
22
+ });
23
+
24
+ return new Promise((resolve) => {
25
+ console.log(`\n${prompt}`);
26
+ options.forEach((opt, idx) => {
27
+ console.log(` ${idx + 1}. ${opt}`);
28
+ });
29
+ console.log(` 0. Done/Skip`);
30
+
31
+ rl.question('\nEnter number: ', (answer) => {
32
+ rl.close();
33
+ const num = parseInt(answer, 10);
34
+ if (num === 0) {
35
+ resolve(null);
36
+ } else if (num > 0 && num <= options.length) {
37
+ resolve(options[num - 1]);
38
+ } else {
39
+ console.log('Invalid selection');
40
+ resolve(null);
41
+ }
42
+ });
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Get multiple selections
48
+ */
49
+ async function getMultipleSelections(prompt, options) {
50
+ const selections = [];
51
+ let done = false;
52
+
53
+ while (!done) {
54
+ const remaining = options.filter(opt => !selections.includes(opt));
55
+ if (remaining.length === 0) {
56
+ break;
57
+ }
58
+
59
+ const selection = await getSelection(
60
+ selections.length === 0 ? prompt : `${prompt} (Selected: ${selections.join(', ')})`,
61
+ remaining
62
+ );
63
+
64
+ if (selection === null) {
65
+ done = true;
66
+ } else {
67
+ selections.push(selection);
68
+ }
69
+ }
70
+
71
+ return selections;
72
+ }
73
+
74
+ /**
75
+ * Format frontmatter list
76
+ */
77
+ function formatList(items) {
78
+ if (items.length === 0) {
79
+ return ' -';
80
+ }
81
+ return items.map(item => ` - ${item}`).join('\n');
82
+ }
83
+
84
+ /**
85
+ * Main execution
86
+ */
87
+ async function main() {
88
+ config.requireHugoSite();
89
+
90
+ console.log('=== Hugo Draft Tags & Categories Helper ===\n');
91
+
92
+ // Check if data file exists
93
+ if (!fs.existsSync(DATA_FILE)) {
94
+ console.error(`Error: Data file not found at ${DATA_FILE}`);
95
+ console.error('Run "extract-tags" first to generate the data file.');
96
+ process.exit(1);
97
+ }
98
+
99
+ // Load data
100
+ const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
101
+
102
+ console.log(`Loaded ${data.categories.length} categories and ${data.tags.length} tags`);
103
+ console.log(`(Generated from ${data.stats.totalFiles} posts on ${new Date(data.stats.generatedAt).toLocaleDateString()})\n`);
104
+
105
+ // Get category selection
106
+ const category = await getSelection(
107
+ 'Select a category:',
108
+ data.categories
109
+ );
110
+
111
+ // Get tag selections
112
+ const tags = await getMultipleSelections(
113
+ 'Select tags (you can select multiple):',
114
+ data.tags
115
+ );
116
+
117
+ // Generate output
118
+ console.log('\n=== Generated Frontmatter ===\n');
119
+
120
+ if (category) {
121
+ console.log('categories:');
122
+ console.log(` - ${category}`);
123
+ } else {
124
+ console.log('categories:');
125
+ console.log(' -');
126
+ }
127
+
128
+ console.log('tags:');
129
+ console.log(formatList(tags));
130
+
131
+ console.log('\n=== Copy the above into your draft frontmatter ===\n');
132
+ }
133
+
134
+ // Handle Ctrl+C gracefully
135
+ process.on('SIGINT', () => {
136
+ console.log('\n\nCancelled.');
137
+ process.exit(0);
138
+ });
139
+
140
+ main().catch(err => {
141
+ console.error('Error:', err);
142
+ process.exit(1);
143
+ });