@kntic/links 0.1.0 → 0.2.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 +1 -1
- package/src/cli.js +4 -0
- package/src/commands/add.js +19 -1
- package/src/commands/edit.js +167 -0
- package/src/commands/reorder.js +263 -0
- package/src/config.js +7 -15
- package/src/generator.js +2 -22
- package/src/utils.js +4 -20
package/package.json
CHANGED
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();
|
package/src/commands/add.js
CHANGED
|
@@ -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 —
|
|
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
|
|
|
@@ -70,27 +71,6 @@ export function filterScheduled(links, now = new Date()) {
|
|
|
70
71
|
return links.filter((link) => isLinkActive(link, now));
|
|
71
72
|
}
|
|
72
73
|
|
|
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
74
|
// ---------------------------------------------------------------------------
|
|
95
75
|
// HTML generation
|
|
96
76
|
// ---------------------------------------------------------------------------
|
|
@@ -113,7 +93,7 @@ export function generatePage(config, options = {}) {
|
|
|
113
93
|
const theme = config.theme || 'minimal-dark';
|
|
114
94
|
|
|
115
95
|
// Load and inline CSS
|
|
116
|
-
const css =
|
|
96
|
+
const css = loadTheme(theme);
|
|
117
97
|
|
|
118
98
|
// Filter links by schedule
|
|
119
99
|
const allLinks = config.links || [];
|
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
|