@chriswiegman/hugo-tools 0.1.2 → 0.2.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
6
6
  and is generated by [Changie](https://github.com/miniscruff/changie).
7
7
 
8
+ ## 0.2.1 - 2026-07-04
9
+
10
+ ### Chores
11
+
12
+ * Add Eslint file formatting for easier development
13
+ * Correct all linting issues with new formatter.
14
+
15
+ ## 0.2.0 - 2026-05-09
16
+
17
+ ### Features
18
+
19
+ * Rework pick-image to move images into the dated assets directory and insert references directly into content files (frontmatter images array or markdown image tag)
20
+
21
+ ### Chores
22
+
23
+ * Update project dependencies.
24
+
8
25
  ## 0.1.2 - 2026-05-04
9
26
 
10
27
  ### Features
package/README.md CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
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
4
 
5
+ > **Never invoke these with `node path/to/script.js`.** Every command below is registered as a proper CLI via the `bin` field in `package.json`, so once installed you run it by name (`draft`, `publish`, ...) — see [Installation](#installation) for the three ways to do that.
6
+
5
7
  ## Requirements
6
8
 
7
9
  - Node.js 18 or later
8
10
 
9
11
  ## Installation
10
12
 
13
+ Whichever route you pick, you're done as soon as you can run a bare command name (`draft`, `publish`, ...) — you should never need to reference a file path under `node_modules/` directly.
14
+
11
15
  **Run without installing (recommended):**
12
16
 
13
17
  ```sh
@@ -27,7 +31,14 @@ npx -p @chriswiegman/hugo-tools publish content/drafts/my-post.md now
27
31
  npm install --save-dev @chriswiegman/hugo-tools
28
32
  ```
29
33
 
30
- Then run commands via `npx` (no `-p` flag needed once installed) or add them as scripts in your `package.json`:
34
+ Then run commands via `npx` (no `-p` flag needed once installed):
35
+
36
+ ```sh
37
+ npx draft
38
+ npx publish content/drafts/my-post.md now
39
+ ```
40
+
41
+ ...or add them as scripts in your `package.json` so `npm run publish` works too:
31
42
 
32
43
  ```json
33
44
  {
@@ -196,13 +207,34 @@ Requires `extract-tags` to have been run at least once.
196
207
 
197
208
  ### `pick-image`
198
209
 
199
- Lists images in your `imagesDir` for the current year/month, lets you pick one, and copies the Hugo-relative path to the clipboard.
210
+ Moves an image into your `imagesDir` under the current year/month and inserts a reference into a Hugo content file.
211
+
212
+ ```sh
213
+ pick-image <image-file> [content-file]
214
+ ```
215
+
216
+ **What it does:**
217
+
218
+ - Moves `<image-file>` to `assets/images/YYYY/MM/<filename>` (creates the directory if needed)
219
+ - Derives the Hugo URL path by stripping the `assets/` prefix (e.g. `/images/YYYY/MM/filename`)
220
+ - If a `[content-file]` is given:
221
+ - Appends the path to the `images:` array in front matter when that field is present (replacing any placeholder ` -` entry)
222
+ - Otherwise appends a `![](path)` markdown tag to the end of the body
223
+ - If no `[content-file]` is given, copies the URL path to the clipboard instead
224
+
225
+ **Examples:**
200
226
 
201
227
  ```sh
202
- pick-image
228
+ # Move photo.jpg and insert into the currently open draft
229
+ pick-image ~/Downloads/photo.jpg content/drafts/my-post.md
230
+
231
+ # Move photo.jpg and copy the path to the clipboard
232
+ pick-image ~/Downloads/photo.jpg
203
233
  ```
204
234
 
205
- Clipboard support: macOS (`pbcopy`), Windows (`clip`), Linux (`xclip`, `xsel`, or `wl-copy`whichever is available).
235
+ The VS Code "Pick Image" task prompts for the image path and automatically passes the currently open file as the content target drag the image file into the terminal prompt to fill the path.
236
+
237
+ Clipboard support (no-content-file mode): macOS (`pbcopy`), Windows (`clip`), Linux (`xclip`, `xsel`, or `wl-copy` — whichever is available).
206
238
 
207
239
  ---
208
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriswiegman/hugo-tools",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "A set of tools to make blogging easier with Hugo.",
5
5
  "keywords": [
6
6
  "hugo",
@@ -41,12 +41,13 @@
41
41
  },
42
42
  "scripts": {
43
43
  "lint": "eslint src/",
44
- "test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs"
44
+ "test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs src/pick-image.test.mjs"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@eslint/js": "^10.0.1",
48
- "changie": "^1.24.0",
49
- "eslint": "^10.3.0",
50
- "globals": "^17.6.0"
48
+ "@stylistic/eslint-plugin": "^5.10.0",
49
+ "changie": "^1.25.0",
50
+ "eslint": "^10.6.0",
51
+ "globals": "^17.7.0"
51
52
  }
52
- }
53
+ }
package/src/add-tags.js CHANGED
@@ -16,128 +16,131 @@ const DATA_FILE = path.join(process.cwd(), config.vscodeDir, 'tags-categories.js
16
16
  * Display menu and get user selection
17
17
  */
18
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
- });
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
+
35
+ if (num === 0) {
36
+ resolve(null);
37
+ } else if (num > 0 && num <= options.length) {
38
+ resolve(options[num - 1]);
39
+ } else {
40
+ console.log('Invalid selection');
41
+ resolve(null);
42
+ }
43
+ });
44
+ });
44
45
  }
45
46
 
46
47
  /**
47
48
  * Get multiple selections
48
49
  */
49
50
  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;
51
+ const selections = [];
52
+ let done = false;
53
+
54
+ while (!done) {
55
+ const remaining = options.filter((opt) => !selections.includes(opt));
56
+
57
+ if (remaining.length === 0) {
58
+ break;
59
+ }
60
+
61
+ const selection = await getSelection(
62
+ selections.length === 0 ? prompt : `${prompt} (Selected: ${selections.join(', ')})`,
63
+ remaining,
64
+ );
65
+
66
+ if (selection === null) {
67
+ done = true;
68
+ } else {
69
+ selections.push(selection);
70
+ }
71
+ }
72
+
73
+ return selections;
72
74
  }
73
75
 
74
76
  /**
75
77
  * Format frontmatter list
76
78
  */
77
79
  function formatList(items) {
78
- if (items.length === 0) {
79
- return ' -';
80
- }
81
- return items.map(item => ` - ${item}`).join('\n');
80
+ if (items.length === 0) {
81
+ return ' -';
82
+ }
83
+
84
+ return items.map((item) => ` - ${item}`).join('\n');
82
85
  }
83
86
 
84
87
  /**
85
88
  * Main execution
86
89
  */
87
90
  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');
91
+ config.requireHugoSite();
92
+
93
+ console.log('=== Hugo Draft Tags & Categories Helper ===\n');
94
+
95
+ // Check if data file exists
96
+ if (!fs.existsSync(DATA_FILE)) {
97
+ console.error(`Error: Data file not found at ${DATA_FILE}`);
98
+ console.error('Run "extract-tags" first to generate the data file.');
99
+ process.exit(1);
100
+ }
101
+
102
+ // Load data
103
+ const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
104
+
105
+ console.log(`Loaded ${data.categories.length} categories and ${data.tags.length} tags`);
106
+ console.log(`(Generated from ${data.stats.totalFiles} posts on ${new Date(data.stats.generatedAt).toLocaleDateString()})\n`);
107
+
108
+ // Get category selection
109
+ const category = await getSelection(
110
+ 'Select a category:',
111
+ data.categories,
112
+ );
113
+
114
+ // Get tag selections
115
+ const tags = await getMultipleSelections(
116
+ 'Select tags (you can select multiple):',
117
+ data.tags,
118
+ );
119
+
120
+ // Generate output
121
+ console.log('\n=== Generated Frontmatter ===\n');
122
+
123
+ if (category) {
124
+ console.log('categories:');
125
+ console.log(` - ${category}`);
126
+ } else {
127
+ console.log('categories:');
128
+ console.log(' -');
129
+ }
130
+
131
+ console.log('tags:');
132
+ console.log(formatList(tags));
133
+
134
+ console.log('\n=== Copy the above into your draft frontmatter ===\n');
132
135
  }
133
136
 
134
137
  // Handle Ctrl+C gracefully
135
138
  process.on('SIGINT', () => {
136
- console.log('\n\nCancelled.');
137
- process.exit(0);
139
+ console.log('\n\nCancelled.');
140
+ process.exit(0);
138
141
  });
139
142
 
140
- main().catch(err => {
141
- console.error('Error:', err);
142
- process.exit(1);
143
+ main().catch((err) => {
144
+ console.error('Error:', err);
145
+ process.exit(1);
143
146
  });