@kntic/links 0.1.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kntic/links",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for building and deploying self-hosted link-in-bio pages. No accounts, no tracking — just HTML.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.js CHANGED
@@ -13,6 +13,8 @@ import { registerQr } from './commands/qr.js';
13
13
  import { registerConfig } from './commands/config-cmd.js';
14
14
  import { registerOpen } from './commands/open-cmd.js';
15
15
  import { registerStatus } from './commands/status.js';
16
+ import { registerReorder } from './commands/reorder.js';
17
+ import { registerEdit } from './commands/edit.js';
16
18
 
17
19
  const require = createRequire(import.meta.url);
18
20
  const { version } = require('../package.json');
@@ -35,5 +37,7 @@ registerQr(program);
35
37
  registerConfig(program);
36
38
  registerOpen(program);
37
39
  registerStatus(program);
40
+ registerReorder(program);
41
+ registerEdit(program);
38
42
 
39
43
  program.parse();
@@ -5,7 +5,7 @@
5
5
  * atomically. Validates the URL and rejects duplicate labels.
6
6
  */
7
7
 
8
- import { findConfig, readConfig, writeConfig, validateUrl } from '../config.js';
8
+ import { findConfig, readConfig, writeConfig, validateUrl, validateDate } from '../config.js';
9
9
 
10
10
  export function registerAdd(program) {
11
11
  program
@@ -25,6 +25,24 @@ export function registerAdd(program) {
25
25
  return;
26
26
  }
27
27
 
28
+ // Validate schedule dates (if provided)
29
+ if (opts.from) {
30
+ const fromCheck = validateDate(opts.from);
31
+ if (!fromCheck.valid) {
32
+ console.error(`Error: --from: ${fromCheck.error}`);
33
+ process.exitCode = 1;
34
+ return;
35
+ }
36
+ }
37
+ if (opts.until) {
38
+ const untilCheck = validateDate(opts.until);
39
+ if (!untilCheck.valid) {
40
+ console.error(`Error: --until: ${untilCheck.error}`);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+ }
45
+
28
46
  // Find and read config
29
47
  let configPath;
30
48
  let config;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * links edit — Edit an existing link's properties in-place.
3
+ *
4
+ * Only specified flags are changed; everything else is left untouched.
5
+ * Supports --no-<flag> to remove optional fields entirely.
6
+ */
7
+
8
+ import { findConfig, readConfig, writeConfig, validateUrl, validateDate } from '../config.js';
9
+
10
+ export function registerEdit(program) {
11
+ program
12
+ .command('edit <label>')
13
+ .description('Edit an existing link')
14
+ .option('--label <new-label>', 'Rename the link')
15
+ .option('--url <url>', 'Update the URL')
16
+ .option('--icon <emoji>', 'Set or update the icon')
17
+ .option('--no-icon', 'Remove the icon')
18
+ .option('--description <text>', 'Set or update the description')
19
+ .option('--no-description', 'Remove the description')
20
+ .option('--from <date>', 'Set or update scheduled_from (ISO 8601)')
21
+ .option('--no-from', 'Remove scheduled_from')
22
+ .option('--until <date>', 'Set or update scheduled_until (ISO 8601)')
23
+ .option('--no-until', 'Remove scheduled_until')
24
+ .action(async (label, opts) => {
25
+ // Detect whether any editing flags were actually provided.
26
+ // Commander sets --no-icon to icon=false, --icon <v> to icon=<v>,
27
+ // and leaves icon=undefined when neither is passed. However,
28
+ // Commander pre-populates boolean defaults for --no-* pairs:
29
+ // when neither --icon nor --no-icon is passed, opts.icon is true (default).
30
+ // We need to check rawArgs or compare against defaults.
31
+ const hasLabel = opts.label !== undefined;
32
+ const hasUrl = opts.url !== undefined;
33
+ // For --no-* pairs: Commander sets default to true. Explicitly passed
34
+ // --icon <val> gives a string; --no-icon gives false; default is true.
35
+ const hasIcon = typeof opts.icon === 'string' || opts.icon === false;
36
+ const hasDescription = typeof opts.description === 'string' || opts.description === false;
37
+ const hasFrom = typeof opts.from === 'string' || opts.from === false;
38
+ const hasUntil = typeof opts.until === 'string' || opts.until === false;
39
+
40
+ const hasAnyOption = hasLabel || hasUrl || hasIcon || hasDescription || hasFrom || hasUntil;
41
+
42
+ if (!hasAnyOption) {
43
+ console.log(
44
+ 'Usage: links edit <label> [--url <url>] [--label <new>] [--icon <emoji>] [--description <text>] [--from <date>] [--until <date>]',
45
+ );
46
+ return;
47
+ }
48
+
49
+ // Validate inputs before touching config
50
+ if (hasUrl) {
51
+ const urlCheck = validateUrl(opts.url);
52
+ if (!urlCheck.valid) {
53
+ console.error(`Error: ${urlCheck.error}`);
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ }
58
+ if (typeof opts.from === 'string') {
59
+ const fromCheck = validateDate(opts.from);
60
+ if (!fromCheck.valid) {
61
+ console.error(`Error: --from: ${fromCheck.error}`);
62
+ process.exitCode = 1;
63
+ return;
64
+ }
65
+ }
66
+ if (typeof opts.until === 'string') {
67
+ const untilCheck = validateDate(opts.until);
68
+ if (!untilCheck.valid) {
69
+ console.error(`Error: --until: ${untilCheck.error}`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ }
74
+
75
+ // Find and read config
76
+ let configPath;
77
+ let config;
78
+ try {
79
+ configPath = findConfig();
80
+ config = readConfig(configPath);
81
+ } catch (err) {
82
+ console.error(`Error: ${err.message}`);
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+
87
+ if (!config.links) {
88
+ config.links = [];
89
+ }
90
+
91
+ // Find link by label (case-insensitive)
92
+ const existingIndex = config.links.findIndex(
93
+ (l) => l.label.toLowerCase() === label.toLowerCase(),
94
+ );
95
+
96
+ if (existingIndex === -1) {
97
+ console.error(`No link with label "${label}" found.`);
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+
102
+ // Check rename collision
103
+ if (hasLabel) {
104
+ const collision = config.links.findIndex(
105
+ (l, i) => i !== existingIndex && l.label.toLowerCase() === opts.label.toLowerCase(),
106
+ );
107
+ if (collision !== -1) {
108
+ console.error(`A link with label "${opts.label}" already exists.`);
109
+ process.exitCode = 1;
110
+ return;
111
+ }
112
+ }
113
+
114
+ const link = config.links[existingIndex];
115
+ const oldLabel = link.label;
116
+ const renamed = hasLabel && opts.label !== oldLabel;
117
+
118
+ // Apply changes — only specified flags
119
+ if (hasLabel) link.label = opts.label;
120
+ if (hasUrl) link.url = opts.url;
121
+
122
+ if (typeof opts.icon === 'string') {
123
+ link.icon = opts.icon;
124
+ } else if (opts.icon === false) {
125
+ delete link.icon;
126
+ }
127
+
128
+ if (typeof opts.description === 'string') {
129
+ link.description = opts.description;
130
+ } else if (opts.description === false) {
131
+ delete link.description;
132
+ }
133
+
134
+ if (typeof opts.from === 'string') {
135
+ link.scheduled_from = opts.from;
136
+ } else if (opts.from === false) {
137
+ delete link.scheduled_from;
138
+ }
139
+
140
+ if (typeof opts.until === 'string') {
141
+ link.scheduled_until = opts.until;
142
+ } else if (opts.until === false) {
143
+ delete link.scheduled_until;
144
+ }
145
+
146
+ // Write back
147
+ try {
148
+ writeConfig(configPath, config);
149
+ } catch (err) {
150
+ console.error(`Error: could not write links.yaml — ${err.message}`);
151
+ process.exitCode = 1;
152
+ return;
153
+ }
154
+
155
+ // Output message
156
+ if (renamed) {
157
+ const otherChanges = hasUrl || hasIcon || hasDescription || hasFrom || hasUntil;
158
+ if (otherChanges) {
159
+ console.log(`✓ Renamed "${oldLabel}" → "${link.label}" and updated.`);
160
+ } else {
161
+ console.log(`✓ Renamed "${oldLabel}" → "${link.label}"`);
162
+ }
163
+ } else {
164
+ console.log(`✓ Updated: ${link.label}`);
165
+ }
166
+ });
167
+ }
@@ -0,0 +1,263 @@
1
+ /**
2
+ * links reorder — Reorder links in links.yaml.
3
+ *
4
+ * Subcommands:
5
+ * (no args) — print current order with 1-based indices
6
+ * move <label> <pos> — move a link to the given 1-based position
7
+ * up <label> — move a link one position up
8
+ * down <label> — move a link one position down
9
+ * set <labels...> — specify the complete new order by labels
10
+ */
11
+
12
+ import { findConfig, readConfig, writeConfig } from '../config.js';
13
+
14
+ /**
15
+ * Print the current link order as a numbered list.
16
+ * @param {object[]} links
17
+ */
18
+ function printOrder(links) {
19
+ if (links.length === 0) {
20
+ console.log('No links configured.');
21
+ return;
22
+ }
23
+ links.forEach((link, i) => {
24
+ console.log(`${i + 1}. ${link.label}`);
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Find a link index by label (case-insensitive).
30
+ * @param {object[]} links
31
+ * @param {string} label
32
+ * @returns {number} index or -1
33
+ */
34
+ function findByLabel(links, label) {
35
+ return links.findIndex(
36
+ (l) => l.label.toLowerCase() === label.toLowerCase(),
37
+ );
38
+ }
39
+
40
+ export function registerReorder(program) {
41
+ const reorderCmd = program
42
+ .command('reorder')
43
+ .description('Reorder links in links.yaml')
44
+ .action(async () => {
45
+ // No subcommand — print current order
46
+ let config;
47
+ try {
48
+ const configPath = findConfig();
49
+ config = readConfig(configPath);
50
+ } catch (err) {
51
+ console.error(`Error: ${err.message}`);
52
+ process.exitCode = 1;
53
+ return;
54
+ }
55
+
56
+ printOrder(config.links || []);
57
+ });
58
+
59
+ // --- reorder move <label> <position> ---
60
+ reorderCmd
61
+ .command('move <label> <position>')
62
+ .description('Move a link to the given 1-based position')
63
+ .action(async (label, position) => {
64
+ let configPath, config;
65
+ try {
66
+ configPath = findConfig();
67
+ config = readConfig(configPath);
68
+ } catch (err) {
69
+ console.error(`Error: ${err.message}`);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ const links = config.links || [];
75
+ const index = findByLabel(links, label);
76
+ if (index === -1) {
77
+ console.error(`No link with label "${label}" found.`);
78
+ process.exitCode = 1;
79
+ return;
80
+ }
81
+
82
+ const pos = parseInt(position, 10);
83
+ if (Number.isNaN(pos) || pos < 1 || pos > links.length) {
84
+ console.error(`Invalid position: ${position}. Must be between 1 and ${links.length}.`);
85
+ process.exitCode = 1;
86
+ return;
87
+ }
88
+
89
+ // Splice out and insert at new position
90
+ const [item] = links.splice(index, 1);
91
+ links.splice(pos - 1, 0, item);
92
+ config.links = links;
93
+
94
+ try {
95
+ writeConfig(configPath, config);
96
+ } catch (err) {
97
+ console.error(`Error: could not write links.yaml — ${err.message}`);
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+
102
+ console.log(`✓ Moved "${item.label}" to position ${pos}.`);
103
+ printOrder(links);
104
+ });
105
+
106
+ // --- reorder up <label> ---
107
+ reorderCmd
108
+ .command('up <label>')
109
+ .description('Move a link one position up')
110
+ .action(async (label) => {
111
+ let configPath, config;
112
+ try {
113
+ configPath = findConfig();
114
+ config = readConfig(configPath);
115
+ } catch (err) {
116
+ console.error(`Error: ${err.message}`);
117
+ process.exitCode = 1;
118
+ return;
119
+ }
120
+
121
+ const links = config.links || [];
122
+ const index = findByLabel(links, label);
123
+ if (index === -1) {
124
+ console.error(`No link with label "${label}" found.`);
125
+ process.exitCode = 1;
126
+ return;
127
+ }
128
+
129
+ if (index === 0) {
130
+ console.log(`${links[index].label} is already at the top.`);
131
+ return;
132
+ }
133
+
134
+ // Swap with previous
135
+ [links[index - 1], links[index]] = [links[index], links[index - 1]];
136
+ config.links = links;
137
+
138
+ try {
139
+ writeConfig(configPath, config);
140
+ } catch (err) {
141
+ console.error(`Error: could not write links.yaml — ${err.message}`);
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+
146
+ console.log(`✓ Moved "${links[index - 1].label}" up to position ${index}.`);
147
+ printOrder(links);
148
+ });
149
+
150
+ // --- reorder down <label> ---
151
+ reorderCmd
152
+ .command('down <label>')
153
+ .description('Move a link one position down')
154
+ .action(async (label) => {
155
+ let configPath, config;
156
+ try {
157
+ configPath = findConfig();
158
+ config = readConfig(configPath);
159
+ } catch (err) {
160
+ console.error(`Error: ${err.message}`);
161
+ process.exitCode = 1;
162
+ return;
163
+ }
164
+
165
+ const links = config.links || [];
166
+ const index = findByLabel(links, label);
167
+ if (index === -1) {
168
+ console.error(`No link with label "${label}" found.`);
169
+ process.exitCode = 1;
170
+ return;
171
+ }
172
+
173
+ if (index === links.length - 1) {
174
+ console.log(`${links[index].label} is already at the bottom.`);
175
+ return;
176
+ }
177
+
178
+ // Swap with next
179
+ [links[index], links[index + 1]] = [links[index + 1], links[index]];
180
+ config.links = links;
181
+
182
+ try {
183
+ writeConfig(configPath, config);
184
+ } catch (err) {
185
+ console.error(`Error: could not write links.yaml — ${err.message}`);
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+
190
+ console.log(`✓ Moved "${links[index + 1].label}" down to position ${index + 2}.`);
191
+ printOrder(links);
192
+ });
193
+
194
+ // --- reorder set <labels...> ---
195
+ reorderCmd
196
+ .command('set <labels...>')
197
+ .description('Specify the complete new order by passing all labels')
198
+ .action(async (labels) => {
199
+ let configPath, config;
200
+ try {
201
+ configPath = findConfig();
202
+ config = readConfig(configPath);
203
+ } catch (err) {
204
+ console.error(`Error: ${err.message}`);
205
+ process.exitCode = 1;
206
+ return;
207
+ }
208
+
209
+ const links = config.links || [];
210
+
211
+ if (labels.length !== links.length) {
212
+ console.error(
213
+ `Expected ${links.length} labels but got ${labels.length}. All existing labels must be specified.`,
214
+ );
215
+ process.exitCode = 1;
216
+ return;
217
+ }
218
+
219
+ // Validate all provided labels exist and build new order
220
+ const newLinks = [];
221
+ const used = new Set();
222
+
223
+ for (const label of labels) {
224
+ const index = findByLabel(links, label);
225
+ if (index === -1) {
226
+ console.error(`No link with label "${label}" found.`);
227
+ process.exitCode = 1;
228
+ return;
229
+ }
230
+
231
+ const key = links[index].label.toLowerCase();
232
+ if (used.has(key)) {
233
+ console.error(`Duplicate label "${label}" in arguments.`);
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ used.add(key);
238
+ newLinks.push(links[index]);
239
+ }
240
+
241
+ // Verify all existing labels are covered
242
+ for (const link of links) {
243
+ if (!used.has(link.label.toLowerCase())) {
244
+ console.error(`Missing label "${link.label}" — all existing labels must be specified.`);
245
+ process.exitCode = 1;
246
+ return;
247
+ }
248
+ }
249
+
250
+ config.links = newLinks;
251
+
252
+ try {
253
+ writeConfig(configPath, config);
254
+ } catch (err) {
255
+ console.error(`Error: could not write links.yaml — ${err.message}`);
256
+ process.exitCode = 1;
257
+ return;
258
+ }
259
+
260
+ console.log('✓ Links reordered.');
261
+ printOrder(newLinks);
262
+ });
263
+ }
package/src/config.js CHANGED
@@ -1,20 +1,7 @@
1
- import { readFileSync, writeFileSync, renameSync } from 'node:fs';
1
+ import { readFileSync, writeFileSync, renameSync, existsSync } from 'node:fs';
2
2
  import { resolve, dirname, join } from 'node:path';
3
3
  import yaml from 'js-yaml';
4
4
 
5
- // ---------------------------------------------------------------------------
6
- // Schema defaults
7
- // ---------------------------------------------------------------------------
8
-
9
- export const DEFAULT_CONFIG = {
10
- name: 'My Links',
11
- bio: '',
12
- avatar: '',
13
- theme: 'minimal-dark',
14
- domain: '',
15
- links: [],
16
- };
17
-
18
5
  // ---------------------------------------------------------------------------
19
6
  // Validation helpers
20
7
  // ---------------------------------------------------------------------------
@@ -154,7 +141,12 @@ export function findConfig(startDir = process.cwd()) {
154
141
  readFileSync(candidate); // existence check
155
142
  return candidate;
156
143
  } catch {
157
- // not found — go up
144
+ // not found — check for git root boundary before climbing
145
+ if (existsSync(join(dir, '.git'))) {
146
+ throw new Error(
147
+ 'links.yaml not found (stopped at git root). Run "links init" to create a new project.',
148
+ );
149
+ }
158
150
  }
159
151
 
160
152
  const parent = dirname(dir);
package/src/generator.js CHANGED
@@ -9,6 +9,7 @@ import { readFileSync } from 'node:fs';
9
9
  import { resolve, dirname, extname } from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
11
  import { isLinkActive } from './utils.js';
12
+ import { loadTheme } from './themes/loader.js';
12
13
 
13
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
14
15
 
@@ -27,6 +28,15 @@ function esc(str) {
27
28
  .replace(/'/g, '&#39;');
28
29
  }
29
30
 
31
+ /**
32
+ * Extract the --accent-color value from a CSS string.
33
+ * Falls back to '#6366f1' (indigo) if not found.
34
+ */
35
+ export function extractAccentColor(css) {
36
+ const match = css.match(/--accent-color:\s*([^;]+);/);
37
+ return match ? match[1].trim() : '#6366f1';
38
+ }
39
+
30
40
  /** MIME type lookup for common image formats. */
31
41
  function imageMime(filePath) {
32
42
  const ext = extname(filePath).toLowerCase();
@@ -70,27 +80,6 @@ export function filterScheduled(links, now = new Date()) {
70
80
  return links.filter((link) => isLinkActive(link, now));
71
81
  }
72
82
 
73
- // ---------------------------------------------------------------------------
74
- // Theme CSS loading
75
- // ---------------------------------------------------------------------------
76
-
77
- /**
78
- * Load theme CSS from the themes directory.
79
- * @param {string} themeName — name without .css extension
80
- * @returns {string} CSS content
81
- */
82
- export function loadThemeCSS(themeName) {
83
- const themePath = resolve(__dirname, 'themes', `${themeName}.css`);
84
- try {
85
- return readFileSync(themePath, 'utf8');
86
- } catch {
87
- throw new Error(
88
- `Theme "${themeName}" not found at ${themePath}. ` +
89
- 'Available themes live in src/themes/.',
90
- );
91
- }
92
- }
93
-
94
83
  // ---------------------------------------------------------------------------
95
84
  // HTML generation
96
85
  // ---------------------------------------------------------------------------
@@ -113,7 +102,7 @@ export function generatePage(config, options = {}) {
113
102
  const theme = config.theme || 'minimal-dark';
114
103
 
115
104
  // Load and inline CSS
116
- const css = loadThemeCSS(theme);
105
+ const css = loadTheme(theme);
117
106
 
118
107
  // Filter links by schedule
119
108
  const allLinks = config.links || [];
@@ -124,16 +113,37 @@ export function generatePage(config, options = {}) {
124
113
 
125
114
  // Avatar handling
126
115
  let avatarHTML = '';
116
+ let avatarDataUri = null;
127
117
  if (config.avatar && config.avatar.trim().length > 0) {
128
118
  try {
129
- const dataUri = inlineImage(config.avatar, configDir);
130
- avatarHTML = `<img class="profile__avatar" src="${dataUri}" alt="${esc(name)}" width="88" height="88">`;
119
+ avatarDataUri = inlineImage(config.avatar, configDir);
120
+ avatarHTML = `<img class="profile__avatar" src="${avatarDataUri}" alt="${esc(name)}" width="88" height="88">`;
131
121
  } catch {
132
122
  // Fallback: reference the file directly (it will be copied to output dir)
133
123
  avatarHTML = `<img class="profile__avatar" src="${esc(config.avatar)}" alt="${esc(name)}" width="88" height="88">`;
134
124
  }
135
125
  }
136
126
 
127
+ // Favicon handling — Strategy A (avatar) or Strategy B (SVG letter)
128
+ let faviconHTML = '';
129
+ if (avatarDataUri) {
130
+ // Strategy A: reuse the already-computed avatar data URI
131
+ const avatarMime = avatarDataUri.startsWith('data:image/svg') ? 'image/svg+xml' : 'image/png';
132
+ faviconHTML = ` <link rel="icon" type="${avatarMime}" href="${avatarDataUri}">\n`
133
+ + ` <link rel="icon" type="${avatarMime}" sizes="32x32" href="${avatarDataUri}">`;
134
+ } else {
135
+ // Strategy B: generate SVG favicon from first letter of name
136
+ const firstChar = name[0].toUpperCase();
137
+ const accent = extractAccentColor(css);
138
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'>`
139
+ + `<rect width='64' height='64' rx='12' fill='${accent}'/>`
140
+ + `<text x='32' y='32' text-anchor='middle' dominant-baseline='central' font-family='system-ui,sans-serif' font-size='32' font-weight='700' fill='white'>${firstChar}</text>`
141
+ + `</svg>`;
142
+ const svgDataUri = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
143
+ faviconHTML = ` <link rel="icon" type="image/svg+xml" href="${svgDataUri}">\n`
144
+ + ` <link rel="icon" type="image/svg+xml" sizes="32x32" href="${svgDataUri}">`;
145
+ }
146
+
137
147
  // Build link list HTML
138
148
  const linksHTML = links.map((link) => {
139
149
  const icon = link.icon ? `<span class="links__icon">${esc(link.icon)}</span>` : '';
@@ -162,6 +172,7 @@ export function generatePage(config, options = {}) {
162
172
  <meta property="og:title" content="${esc(name)}">
163
173
  <meta property="og:description" content="${esc(ogDescription)}">
164
174
  <meta property="og:type" content="website">
175
+ ${faviconHTML}
165
176
  <style>
166
177
  ${css}
167
178
  </style>
package/src/utils.js CHANGED
@@ -1,27 +1,11 @@
1
1
  /**
2
2
  * Shared utility helpers for @kntic/links.
3
+ *
4
+ * Exports:
5
+ * - fatal(message, code) — pretty-print error and exit
6
+ * - isLinkActive(link, now) — schedule-aware link visibility check
3
7
  */
4
8
 
5
- import { readFile } from 'node:fs/promises';
6
- import { resolve } from 'node:path';
7
- import yaml from 'js-yaml';
8
-
9
- /**
10
- * Default config filename.
11
- */
12
- export const CONFIG_FILE = 'links.yml';
13
-
14
- /**
15
- * Load and parse the links YAML config from the current directory.
16
- * @param {string} [dir=process.cwd()] - Directory to look in.
17
- * @returns {Promise<object>} Parsed config object.
18
- */
19
- export async function loadConfig(dir = process.cwd()) {
20
- const filePath = resolve(dir, CONFIG_FILE);
21
- const raw = await readFile(filePath, 'utf8');
22
- return yaml.load(raw);
23
- }
24
-
25
9
  /**
26
10
  * Pretty-print an error and exit.
27
11
  * @param {string} message