@kntic/links 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/LICENSE +21 -0
- package/README.md +149 -0
- package/package.json +46 -0
- package/src/.gitkeep +0 -0
- package/src/cli.js +39 -0
- package/src/commands/add.js +83 -0
- package/src/commands/config-cmd.js +49 -0
- package/src/commands/deploy.js +97 -0
- package/src/commands/init.js +107 -0
- package/src/commands/list.js +108 -0
- package/src/commands/open-cmd.js +64 -0
- package/src/commands/qr.js +99 -0
- package/src/commands/remove.js +55 -0
- package/src/commands/status.js +64 -0
- package/src/commands/theme.js +86 -0
- package/src/config.js +221 -0
- package/src/generator.js +185 -0
- package/src/themes/.gitkeep +0 -0
- package/src/themes/README.md +110 -0
- package/src/themes/developer.css +210 -0
- package/src/themes/glass.css +215 -0
- package/src/themes/loader.js +61 -0
- package/src/themes/minimal-dark.css +172 -0
- package/src/themes/minimal-light.css +176 -0
- package/src/themes/terminal.css +229 -0
- package/src/utils.js +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 KNTIC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @kntic/links
|
|
2
|
+
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/@kntic/links)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
|
|
7
|
+
A CLI for building and deploying link-in-bio pages. No accounts, no tracking, no third-party servers. You own the HTML.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Five commands from zero to a deployed page:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 1. Install
|
|
17
|
+
npm install -g @kntic/links
|
|
18
|
+
|
|
19
|
+
# 2. Scaffold a new project
|
|
20
|
+
links init my-page && cd my-page
|
|
21
|
+
|
|
22
|
+
# 3. Add some links
|
|
23
|
+
links add "GitHub" "https://github.com/you"
|
|
24
|
+
links add "Blog" "https://your-blog.dev"
|
|
25
|
+
|
|
26
|
+
# 4. Build a self-contained HTML file
|
|
27
|
+
links deploy --self
|
|
28
|
+
|
|
29
|
+
# 5. Open it
|
|
30
|
+
open dist/index.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
That's it. `dist/index.html` is a single file — inline CSS, base64 avatar, no external requests. Drop it on any static host.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g @kntic/links
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Node.js 18 or later.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Command Reference
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `links init [directory]` | Scaffold a new `links.yaml` project. `--force` to overwrite. |
|
|
52
|
+
| `links add <label> <url>` | Add a link. `--from`/`--until` for scheduling, `--update` to replace. |
|
|
53
|
+
| `links remove <label>` | Remove a link by label (case-insensitive). |
|
|
54
|
+
| `links list` | List all links. `--json` for machine-readable output. |
|
|
55
|
+
| `links deploy --self` | Generate a self-contained HTML page to `dist/`. `--out <dir>` to change output, `--open` to open in browser. |
|
|
56
|
+
| `links theme list` | List available themes. |
|
|
57
|
+
| `links theme set <name>` | Set the active theme in `links.yaml`. |
|
|
58
|
+
| `links qr` | Generate a QR code for your page URL. |
|
|
59
|
+
| `links config` | Open `links.yaml` in your `$EDITOR`. |
|
|
60
|
+
| `links open` | Open your deployed page URL in the browser. `--local` for `dist/index.html`. |
|
|
61
|
+
| `links status` | Show project config summary. |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## `links.yaml` Schema
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
# Required
|
|
69
|
+
name: "Your Name"
|
|
70
|
+
url: "https://your-site.com"
|
|
71
|
+
|
|
72
|
+
# Optional
|
|
73
|
+
bio: "A short bio line."
|
|
74
|
+
avatar: "avatar.png" # Path to image — gets base64-inlined on build
|
|
75
|
+
theme: "minimal-dark" # Any theme name from src/themes/
|
|
76
|
+
|
|
77
|
+
# Links
|
|
78
|
+
links:
|
|
79
|
+
- label: "GitHub"
|
|
80
|
+
url: "https://github.com/you"
|
|
81
|
+
- label: "Blog"
|
|
82
|
+
url: "https://your-blog.dev"
|
|
83
|
+
scheduled_from: "2026-04-01" # Optional: link becomes visible on this date
|
|
84
|
+
scheduled_until: "2026-12-31" # Optional: link hidden after this date
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Scheduling
|
|
88
|
+
|
|
89
|
+
Links support `scheduled_from` and `scheduled_until` fields (ISO 8601 date strings). The generator filters links at build time — only active links appear in the output HTML.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Themes
|
|
94
|
+
|
|
95
|
+
Five built-in themes ship with Links:
|
|
96
|
+
|
|
97
|
+
| Theme | Description |
|
|
98
|
+
|-------|-------------|
|
|
99
|
+
| `minimal-dark` | Default. Muted violet accent on dark background. |
|
|
100
|
+
| `minimal-light` | Clean off-white with indigo accent. |
|
|
101
|
+
| `terminal` | Green-on-black with cursor blink and scanlines. |
|
|
102
|
+
| `glass` | Glassmorphism with backdrop-filter blur and purple/blue gradient. |
|
|
103
|
+
| `developer` | IDE-inspired aesthetic with KNTIC orange and left border bar. |
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# List themes
|
|
107
|
+
links theme list
|
|
108
|
+
|
|
109
|
+
# Switch theme
|
|
110
|
+
links theme set terminal
|
|
111
|
+
|
|
112
|
+
# Rebuild
|
|
113
|
+
links deploy --self
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Custom Themes
|
|
117
|
+
|
|
118
|
+
Themes are plain CSS files. Copy any built-in theme and override the 17 CSS custom properties. See [`src/themes/README.md`](src/themes/README.md) for the full token contract.
|
|
119
|
+
|
|
120
|
+
> 📸 **Screenshots coming soon.**
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Self-Hosted by Default
|
|
125
|
+
|
|
126
|
+
`links deploy --self` generates a single HTML file with everything inlined:
|
|
127
|
+
|
|
128
|
+
- CSS is embedded in a `<style>` tag
|
|
129
|
+
- Avatar is base64-encoded into an `<img>` src
|
|
130
|
+
- Zero external requests — no fonts CDN, no analytics, no tracking
|
|
131
|
+
|
|
132
|
+
Upload `dist/index.html` anywhere: Nginx, S3, GitHub Pages, Netlify, a Raspberry Pi — it doesn't matter.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
1. Fork the repo
|
|
139
|
+
2. Create a feature branch
|
|
140
|
+
3. Make your changes (themes are a great first contribution)
|
|
141
|
+
4. Submit a merge request
|
|
142
|
+
|
|
143
|
+
Please keep the zero-dependency-on-external-services philosophy. If it can be done with a single HTML file, it should be.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
[MIT](LICENSE) © 2026 KNTIC
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kntic/links",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for building and deploying self-hosted link-in-bio pages. No accounts, no tracking — just HTML.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"links": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/",
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"linktree",
|
|
20
|
+
"link-page",
|
|
21
|
+
"link-in-bio",
|
|
22
|
+
"cli",
|
|
23
|
+
"privacy",
|
|
24
|
+
"self-hosted",
|
|
25
|
+
"developer",
|
|
26
|
+
"static-site"
|
|
27
|
+
],
|
|
28
|
+
"homepage": "https://kntic.ai",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://gitlab.kommune7.wien/kommune7/apps/links.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://gitlab.kommune7.wien/kommune7/apps/links/-/issues"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"prepublishOnly": "node -e \"import('./src/config.js').then(() => console.log('prepublish check passed'))\""
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^12.1.0",
|
|
42
|
+
"js-yaml": "^4.1.0",
|
|
43
|
+
"open": "^10.1.0",
|
|
44
|
+
"qrcode": "^1.5.4"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/.gitkeep
ADDED
|
File without changes
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
|
|
6
|
+
import { registerInit } from './commands/init.js';
|
|
7
|
+
import { registerAdd } from './commands/add.js';
|
|
8
|
+
import { registerRemove } from './commands/remove.js';
|
|
9
|
+
import { registerList } from './commands/list.js';
|
|
10
|
+
import { registerDeploy } from './commands/deploy.js';
|
|
11
|
+
import { registerTheme } from './commands/theme.js';
|
|
12
|
+
import { registerQr } from './commands/qr.js';
|
|
13
|
+
import { registerConfig } from './commands/config-cmd.js';
|
|
14
|
+
import { registerOpen } from './commands/open-cmd.js';
|
|
15
|
+
import { registerStatus } from './commands/status.js';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const { version } = require('../package.json');
|
|
19
|
+
|
|
20
|
+
const program = new Command();
|
|
21
|
+
|
|
22
|
+
program
|
|
23
|
+
.name('links')
|
|
24
|
+
.description('CLI tool for managing and deploying link-in-bio pages')
|
|
25
|
+
.version(version);
|
|
26
|
+
|
|
27
|
+
// Register all subcommands
|
|
28
|
+
registerInit(program);
|
|
29
|
+
registerAdd(program);
|
|
30
|
+
registerRemove(program);
|
|
31
|
+
registerList(program);
|
|
32
|
+
registerDeploy(program);
|
|
33
|
+
registerTheme(program);
|
|
34
|
+
registerQr(program);
|
|
35
|
+
registerConfig(program);
|
|
36
|
+
registerOpen(program);
|
|
37
|
+
registerStatus(program);
|
|
38
|
+
|
|
39
|
+
program.parse();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* links add — Add a new link entry.
|
|
3
|
+
*
|
|
4
|
+
* Reads the nearest links.yaml, appends a new link, and writes it back
|
|
5
|
+
* atomically. Validates the URL and rejects duplicate labels.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { findConfig, readConfig, writeConfig, validateUrl } from '../config.js';
|
|
9
|
+
|
|
10
|
+
export function registerAdd(program) {
|
|
11
|
+
program
|
|
12
|
+
.command('add <label> <url>')
|
|
13
|
+
.description('Add a new link')
|
|
14
|
+
.option('--icon <emoji>', 'Icon emoji or name for the link')
|
|
15
|
+
.option('--description <text>', 'Short description of the link')
|
|
16
|
+
.option('--from <date>', 'Scheduled start date (ISO 8601)')
|
|
17
|
+
.option('--until <date>', 'Scheduled end date (ISO 8601)')
|
|
18
|
+
.option('--update', 'Replace an existing link with the same label')
|
|
19
|
+
.action(async (label, url, opts) => {
|
|
20
|
+
// Validate URL
|
|
21
|
+
const urlCheck = validateUrl(url);
|
|
22
|
+
if (!urlCheck.valid) {
|
|
23
|
+
console.error(`Error: ${urlCheck.error}`);
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Find and read config
|
|
29
|
+
let configPath;
|
|
30
|
+
let config;
|
|
31
|
+
try {
|
|
32
|
+
configPath = findConfig();
|
|
33
|
+
config = readConfig(configPath);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error(`Error: ${err.message}`);
|
|
36
|
+
process.exitCode = 1;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ensure links array exists
|
|
41
|
+
if (!config.links) {
|
|
42
|
+
config.links = [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Check for duplicate label (case-insensitive)
|
|
46
|
+
const existingIndex = config.links.findIndex(
|
|
47
|
+
(l) => l.label.toLowerCase() === label.toLowerCase(),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
if (existingIndex !== -1 && !opts.update) {
|
|
51
|
+
console.error(
|
|
52
|
+
`A link with label "${label}" already exists. Use --update to replace it.`,
|
|
53
|
+
);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Build link object
|
|
59
|
+
const link = { label, url };
|
|
60
|
+
if (opts.icon) link.icon = opts.icon;
|
|
61
|
+
if (opts.description) link.description = opts.description;
|
|
62
|
+
if (opts.from) link.scheduled_from = opts.from;
|
|
63
|
+
if (opts.until) link.scheduled_until = opts.until;
|
|
64
|
+
|
|
65
|
+
// Add or replace
|
|
66
|
+
if (existingIndex !== -1) {
|
|
67
|
+
config.links[existingIndex] = link;
|
|
68
|
+
} else {
|
|
69
|
+
config.links.push(link);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Write back
|
|
73
|
+
try {
|
|
74
|
+
writeConfig(configPath, config);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.error(`Error: could not write links.yaml — ${err.message}`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(`✓ Added: ${label} → ${url}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* links config — Open links.yaml in the user's preferred editor.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { findConfig } from '../config.js';
|
|
7
|
+
|
|
8
|
+
export function registerConfig(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('config')
|
|
11
|
+
.description('Open links.yaml in your default editor')
|
|
12
|
+
.action(() => {
|
|
13
|
+
let configPath;
|
|
14
|
+
try {
|
|
15
|
+
configPath = findConfig();
|
|
16
|
+
} catch {
|
|
17
|
+
console.error('No links.yaml found. Run: links init');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const editor =
|
|
22
|
+
process.env.EDITOR ||
|
|
23
|
+
process.env.VISUAL ||
|
|
24
|
+
'nano';
|
|
25
|
+
|
|
26
|
+
const result = spawnSync(editor, [configPath], {
|
|
27
|
+
stdio: 'inherit',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (result.error) {
|
|
31
|
+
// If nano wasn't found, try vi as last resort
|
|
32
|
+
if (result.error.code === 'ENOENT' && editor === 'nano') {
|
|
33
|
+
const fallback = spawnSync('vi', [configPath], {
|
|
34
|
+
stdio: 'inherit',
|
|
35
|
+
});
|
|
36
|
+
if (fallback.error) {
|
|
37
|
+
console.error(`Could not open editor. Set $EDITOR and try again.`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
process.exit(fallback.status ?? 0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.error(`Could not open editor "${editor}": ${result.error.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exit(result.status ?? 0);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* links deploy --self — Build a self-contained static HTML page.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, writeFileSync, copyFileSync, existsSync, statSync } from 'node:fs';
|
|
6
|
+
import { resolve, dirname, basename, join } from 'node:path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { findConfig, readConfig } from '../config.js';
|
|
9
|
+
import { generatePage } from '../generator.js';
|
|
10
|
+
|
|
11
|
+
export function registerDeploy(program) {
|
|
12
|
+
program
|
|
13
|
+
.command('deploy')
|
|
14
|
+
.description('Build and deploy the link page')
|
|
15
|
+
.option('--self', 'Generate a self-contained static HTML page')
|
|
16
|
+
.option('--out <dir>', 'Output directory', './dist')
|
|
17
|
+
.option('--open', 'Open the generated page in a browser after build')
|
|
18
|
+
.action(async (opts) => {
|
|
19
|
+
if (!opts.self) {
|
|
20
|
+
console.log(
|
|
21
|
+
chalk.yellow('Usage: links deploy --self [--out <dir>] [--open]'),
|
|
22
|
+
);
|
|
23
|
+
console.log('Other deploy targets are not yet implemented.');
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 1. Locate and read config
|
|
29
|
+
let configPath;
|
|
30
|
+
try {
|
|
31
|
+
configPath = findConfig();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let config;
|
|
39
|
+
try {
|
|
40
|
+
config = readConfig(configPath);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(chalk.red(`✗ ${err.message}`));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const configDir = dirname(configPath);
|
|
48
|
+
|
|
49
|
+
// 2. Generate HTML
|
|
50
|
+
let html;
|
|
51
|
+
try {
|
|
52
|
+
html = generatePage(config, { configDir });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(chalk.red(`✗ Build failed: ${err.message}`));
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. Write output
|
|
60
|
+
const outDir = resolve(opts.out);
|
|
61
|
+
mkdirSync(outDir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
const outFile = join(outDir, 'index.html');
|
|
64
|
+
writeFileSync(outFile, html, 'utf8');
|
|
65
|
+
|
|
66
|
+
// 4. Copy avatar as fallback asset (if it's a local file)
|
|
67
|
+
if (config.avatar && config.avatar.trim().length > 0) {
|
|
68
|
+
const avatarSrc = resolve(configDir, config.avatar);
|
|
69
|
+
if (existsSync(avatarSrc)) {
|
|
70
|
+
try {
|
|
71
|
+
const avatarDest = join(outDir, basename(config.avatar));
|
|
72
|
+
copyFileSync(avatarSrc, avatarDest);
|
|
73
|
+
} catch {
|
|
74
|
+
// non-fatal — avatar is already inlined
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 5. Summary
|
|
80
|
+
const size = statSync(outFile).size;
|
|
81
|
+
const sizeStr = size < 1024
|
|
82
|
+
? `${size} B`
|
|
83
|
+
: `${(size / 1024).toFixed(1)} KB`;
|
|
84
|
+
|
|
85
|
+
console.log(chalk.green(`✓ Built to ${outFile}`) + chalk.dim(` (${sizeStr})`));
|
|
86
|
+
|
|
87
|
+
// 6. Optionally open in browser
|
|
88
|
+
if (opts.open) {
|
|
89
|
+
try {
|
|
90
|
+
const open = (await import('open')).default;
|
|
91
|
+
await open(outFile);
|
|
92
|
+
} catch {
|
|
93
|
+
console.log(chalk.yellow('⚠ Could not open browser automatically.'));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* links init — Scaffold a new link page project.
|
|
3
|
+
*
|
|
4
|
+
* Creates a links.yaml with sensible defaults and prints a getting-started
|
|
5
|
+
* message. Safe to run — never overwrites an existing links.yaml unless
|
|
6
|
+
* --force is passed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { resolve, join } from 'node:path';
|
|
11
|
+
import { userInfo } from 'node:os';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Template
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
function buildTemplate(name) {
|
|
18
|
+
return `# links.yaml — your link-in-bio page configuration
|
|
19
|
+
# Docs: links help
|
|
20
|
+
|
|
21
|
+
name: "${name}"
|
|
22
|
+
|
|
23
|
+
# Short bio displayed under your name
|
|
24
|
+
bio: ""
|
|
25
|
+
|
|
26
|
+
# Avatar image URL (uncomment and set your image URL)
|
|
27
|
+
# avatar: "https://example.com/avatar.png"
|
|
28
|
+
|
|
29
|
+
# Theme for the generated page
|
|
30
|
+
theme: minimal-dark
|
|
31
|
+
|
|
32
|
+
# Your page URL once deployed (uncomment and set your domain)
|
|
33
|
+
# domain: "https://links.example.com"
|
|
34
|
+
|
|
35
|
+
# Links — add as many as you like
|
|
36
|
+
links:
|
|
37
|
+
- label: "My Website"
|
|
38
|
+
url: "https://example.com"
|
|
39
|
+
# icon: "globe"
|
|
40
|
+
# description: "My personal website"
|
|
41
|
+
|
|
42
|
+
- label: "Twitter"
|
|
43
|
+
url: "https://twitter.com/example"
|
|
44
|
+
# icon: "twitter"
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Command
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
export function registerInit(program) {
|
|
53
|
+
program
|
|
54
|
+
.command('init [directory]')
|
|
55
|
+
.description('Initialise a new links project')
|
|
56
|
+
.option('--force', 'Overwrite existing links.yaml', false)
|
|
57
|
+
.action(async (directory, opts) => {
|
|
58
|
+
const targetDir = directory ? resolve(directory) : process.cwd();
|
|
59
|
+
const filePath = join(targetDir, 'links.yaml');
|
|
60
|
+
|
|
61
|
+
// If a directory argument was provided, ensure it exists
|
|
62
|
+
if (directory && !existsSync(targetDir)) {
|
|
63
|
+
try {
|
|
64
|
+
mkdirSync(targetDir, { recursive: true });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error(`Error: could not create directory "${targetDir}" — ${err.message}`);
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Safety check — never overwrite without --force
|
|
73
|
+
if (existsSync(filePath) && !opts.force) {
|
|
74
|
+
console.error('links.yaml already exists. Use --force to overwrite.');
|
|
75
|
+
process.exitCode = 1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Resolve a sensible default name
|
|
80
|
+
let defaultName = 'My Links';
|
|
81
|
+
try {
|
|
82
|
+
const info = userInfo();
|
|
83
|
+
if (info.username) {
|
|
84
|
+
defaultName = info.username;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// userInfo can throw on some platforms — fall back silently
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const content = buildTemplate(defaultName);
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
writeFileSync(filePath, content, 'utf8');
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.error(`Error: could not write links.yaml — ${err.message}`);
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Success message
|
|
101
|
+
const relPath = directory ? join(directory, 'links.yaml') : 'links.yaml';
|
|
102
|
+
console.log(`✔ Created ${relPath}\n`);
|
|
103
|
+
console.log('Next steps:');
|
|
104
|
+
console.log(' links add <label> <url> Add a link');
|
|
105
|
+
console.log(' links deploy --self --out ./dist Build your page');
|
|
106
|
+
});
|
|
107
|
+
}
|