Skip to content

Commit 81e049f

Browse files
committed
feat: install repo-aware lnk cli
1 parent 53eac5b commit 81e049f

5 files changed

Lines changed: 95 additions & 29 deletions

File tree

README.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,14 @@ The CLI intentionally documents only the user-facing environment flags:
6262

6363
* `DRY_RUN=true` prints the planned change without writing, committing, or
6464
pushing.
65-
* `LNK_OWNER=OWNER` changes the default owner field for new links.
66-
* `V8S_LINKS_FILE=FILE` overrides the links file. `LNK_FILE` is supported
67-
as a legacy alias.
65+
* `V8S_REPO=PATH` points copied local helpers at the local repository path.
66+
* `V8S_LINKS_OWNER=OWNER` changes the default owner field for new links.
67+
* `V8S_LINKS_FILE=FILE` overrides the links file.
6868
* `V8S_SCHEDULES_FILE=FILE` overrides the schedule file.
69-
* `V8S_POLICY_FILE=FILE` overrides the block policy file. `BLOCKLIST_FILE`
70-
is supported as a legacy alias.
69+
* `V8S_POLICY_FILE=FILE` overrides the block policy file.
7170

72-
Successful write operations commit and push automatically so Cloudflare can
73-
deploy the generated site.
71+
Successful write operations run `npm run check`, then commit and push
72+
automatically so Cloudflare can deploy the generated site.
7473

7574
## Limitations
7675

defaults/v8s-local-config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"install_path": "$XDG_CONFIG_HOME/zsh/v8s.sh",
66
"rc_file": "$XDG_CONFIG_HOME/zsh/.zshrc"
77
},
8+
"link_cli": {
9+
"install_path": "$XDG_CONFIG_HOME/bin/lnk"
10+
},
811
"registry": {
912
"local_path": "~/.v8s.json"
1013
},

scripts/blocklist-cli.mjs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
import fs from "node:fs";
44
import path from "node:path";
5+
import { spawnSync } from "node:child_process";
56

6-
const POLICY_PATH = process.env.V8S_POLICY_FILE || process.env.BLOCKLIST_FILE || "custom/v8s-policies.json";
7+
const ROOT = process.env.V8S_REPO || process.cwd();
8+
process.chdir(ROOT);
9+
10+
const POLICY_PATH = process.env.V8S_POLICY_FILE || "custom/v8s-policies.json";
711
const CATEGORIES_PATH = "defaults/v8s-blocklist-categories.json";
812

913
function usage() {
@@ -21,8 +25,8 @@ Options:
2125
--help Show this help
2226
2327
Environment:
28+
V8S_REPO=PATH Local vanityURLs/code repository path
2429
V8S_POLICY_FILE=FILE Override the block policy file
25-
BLOCKLIST_FILE=FILE Legacy alias for V8S_POLICY_FILE
2630
2731
Docs:
2832
https://www.VanityURLs.link/en/docs`);
@@ -40,6 +44,17 @@ function writeJson(path, value) {
4044
fs.writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
4145
}
4246

47+
function run(command, args) {
48+
const result = spawnSync(command, args, {
49+
cwd: ROOT,
50+
stdio: "inherit",
51+
shell: process.platform === "win32"
52+
});
53+
54+
if (result.error) throw result.error;
55+
if (result.status !== 0) process.exit(result.status ?? 1);
56+
}
57+
4358
function pathModuleDirname(filePath) {
4459
const dir = path.dirname(filePath);
4560
return dir === "." ? process.cwd() : dir;
@@ -126,13 +141,17 @@ function ensurePolicyShape(policy) {
126141
return policy;
127142
}
128143

129-
function savePolicy(policy, dryRun) {
130-
if (dryRun) {
144+
function savePolicy(policy, dryRun, message) {
145+
if (dryRun || process.env.DRY_RUN === "true") {
131146
console.log(JSON.stringify(policy, null, 2));
132147
return;
133148
}
134149

135150
writeJson(POLICY_PATH, policy);
151+
run("npm", ["run", "check"]);
152+
run("git", ["add", POLICY_PATH]);
153+
run("git", ["commit", "-m", message]);
154+
run("git", ["push"]);
136155
}
137156

138157
function addBlock(domainInput, options) {
@@ -170,7 +189,7 @@ function addBlock(domainInput, options) {
170189
}
171190

172191
policy.block_domains.sort((a, b) => a.domain.localeCompare(b.domain));
173-
savePolicy(policy, options.dryRun);
192+
savePolicy(policy, options.dryRun, `feat(policies): block ${domain}`);
174193

175194
if (!options.dryRun) {
176195
console.log(`Blocked ${domain} as ${category}/${severity}`);
@@ -224,7 +243,7 @@ function addKeyword(keywordInput, options) {
224243
return aKeyword.localeCompare(bKeyword);
225244
});
226245

227-
savePolicy(policy, options.dryRun);
246+
savePolicy(policy, options.dryRun, `feat(policies): block keyword ${keyword}`);
228247

229248
if (!options.dryRun) {
230249
console.log(`Blocked keyword ${keyword} as ${category}/${severity}`);
@@ -259,7 +278,7 @@ function addAllow(domainInput, options) {
259278
policy.allow_domains = [...allowDomains.values()].sort((a, b) => a.domain.localeCompare(b.domain));
260279
policy.block_domains = policy.block_domains.filter((entry) => normalizeHostname(entry.domain) !== domain);
261280

262-
savePolicy(policy, options.dryRun);
281+
savePolicy(policy, options.dryRun, `feat(policies): allow ${domain}`);
263282

264283
if (!options.dryRun) {
265284
const suffix = options.reason ? ` (${options.reason})` : "";

scripts/lnk

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import crypto from "node:crypto";
44
import fs from "node:fs";
55
import { spawnSync } from "node:child_process";
66

7-
const DEFAULT_OWNER = process.env.LNK_OWNER || "bhd";
7+
const ROOT = process.env.V8S_REPO || process.cwd();
8+
process.chdir(ROOT);
9+
10+
const DEFAULT_OWNER = process.env.V8S_LINKS_OWNER || "bhd";
811
const LINKS_FILE = resolveLinksFile();
912
const VALID_STATES = new Set([
1013
"permanent",
@@ -45,21 +48,22 @@ Options:
4548
--help Show this help
4649
4750
Files:
48-
Links are written to custom/v8s-links.txt when it exists, otherwise defaults/v8s-links.txt.
51+
Links are written to custom/v8s-links.txt unless V8S_LINKS_FILE overrides the path.
4952
Schedules are written to custom/v8s-schedules.json.
5053
Block policies are written to custom/v8s-policies.json.
5154
5255
Environment:
5356
DRY_RUN=true Print the planned change without writing, committing, or pushing
54-
LNK_OWNER=OWNER Default owner value for new links
55-
V8S_LINKS_FILE=FILE Override the links file; LNK_FILE is supported as a legacy alias
57+
V8S_REPO=PATH Local vanityURLs/code repository path
58+
V8S_LINKS_OWNER=OWNER Default owner value for new links
59+
V8S_LINKS_FILE=FILE Override the links file
5660
V8S_SCHEDULES_FILE=FILE
5761
Override the schedule file
58-
V8S_POLICY_FILE=FILE Override the block policy file; BLOCKLIST_FILE is a legacy alias
62+
V8S_POLICY_FILE=FILE Override the block policy file
5963
6064
Notes:
6165
Slugs may contain / to create API-like paths.
62-
Successful write operations commit and push automatically.
66+
Successful write operations run npm run check, then commit and push automatically.
6367
6468
Examples:
6569
./scripts/lnk https://github.com/vanityURLs github
@@ -76,9 +80,7 @@ Docs:
7680

7781
function resolveLinksFile() {
7882
if (process.env.V8S_LINKS_FILE) return process.env.V8S_LINKS_FILE;
79-
if (process.env.LNK_FILE) return process.env.LNK_FILE;
80-
if (fs.existsSync("custom/v8s-links.txt")) return "custom/v8s-links.txt";
81-
return "defaults/v8s-links.txt";
83+
return "custom/v8s-links.txt";
8284
}
8385

8486
function run(command, args) {
@@ -211,6 +213,11 @@ function appendLine(filePath, line) {
211213
fs.mkdirSync(dirname(filePath), {
212214
recursive: true
213215
});
216+
217+
if (!fs.existsSync(filePath)) {
218+
fs.writeFileSync(filePath, "# slug|target|state|title|description|tags|owner|expires_at|notes\n");
219+
}
220+
214221
fs.appendFileSync(filePath, `${line}\n`);
215222
}
216223

@@ -330,9 +337,7 @@ function saveScheduleConfig(filePath, config, dryRun, slug) {
330337
}
331338

332339
writeJson(filePath, config);
333-
run("git", ["add", filePath]);
334-
run("git", ["commit", "-m", `feat(schedules): update ${slug}`]);
335-
run("git", ["push"]);
340+
finalizeChange(filePath, `feat(schedules): update ${slug}`);
336341
}
337342

338343
function listSchedules(config, requestedSlug) {
@@ -503,8 +508,13 @@ function main() {
503508

504509
appendLine(LINKS_FILE, line);
505510

506-
run("git", ["add", LINKS_FILE]);
507-
run("git", ["commit", "-m", `feat(links): add ${slug}`]);
511+
finalizeChange(LINKS_FILE, `feat(links): add ${slug}`);
512+
}
513+
514+
function finalizeChange(filePath, message) {
515+
run("npm", ["run", "check"]);
516+
run("git", ["add", filePath]);
517+
run("git", ["commit", "-m", message]);
508518
run("git", ["push"]);
509519
}
510520

scripts/local-install.mjs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const ROOT = process.cwd();
1111
const DEFAULT_CONFIG_PATH = path.join(ROOT, "defaults", "v8s-local-config.json");
1212
const CUSTOM_CONFIG_PATH = path.join(ROOT, "custom", "v8s-local-config.json");
1313
const HELPER_SOURCE_PATH = path.join(ROOT, "scripts", "v8s.sh");
14+
const LNK_SOURCE_PATH = path.join(ROOT, "scripts", "lnk");
1415
const START_MARKER = "# >>> V8S >>>";
1516
const END_MARKER = "# <<< V8S <<<";
1617

@@ -92,6 +93,10 @@ function mergeConfig(base, local) {
9293
...(base.shell_helper || {}),
9394
...(local.shell_helper || {})
9495
},
96+
link_cli: {
97+
...(base.link_cli || {}),
98+
...(local.link_cli || {})
99+
},
95100
registry: {
96101
...(base.registry || {}),
97102
...(local.registry || {})
@@ -118,6 +123,9 @@ async function promptConfig(config, args) {
118123
repository: {
119124
...config.repository,
120125
path: config.repository?.path || ROOT
126+
},
127+
link_cli: {
128+
...config.link_cli
121129
}
122130
};
123131
}
@@ -131,6 +139,9 @@ async function promptConfig(config, args) {
131139
...config.shell_helper,
132140
enabled
133141
},
142+
link_cli: {
143+
...config.link_cli
144+
},
134145
registry: {
135146
...config.registry
136147
},
@@ -142,6 +153,7 @@ async function promptConfig(config, args) {
142153
if (!enabled) return next;
143154

144155
next.shell_helper.install_path = await question(rl, "Shell helper install path", next.shell_helper.install_path);
156+
next.link_cli.install_path = await question(rl, "lnk CLI install path", next.link_cli.install_path);
145157
next.shell_helper.rc_file = await question(rl, "Shell rc file to update", next.shell_helper.rc_file);
146158
next.registry.local_path = await question(rl, "Local registry path", next.registry.local_path);
147159
next.repository.path = await question(rl, "Local repository path", next.repository.path || ROOT);
@@ -182,23 +194,27 @@ function shellQuote(value) {
182194

183195
function installHelper(config, args) {
184196
if (config.shell_helper?.enabled !== true) {
185-
console.log("Shell helper disabled; local config written only.");
197+
console.log("Shell helper disabled; installing lnk CLI only.");
198+
installLnkCli(config, args);
186199
return;
187200
}
188201

189202
const helperTarget = expandLocalPath(config.shell_helper.install_path);
203+
const lnkTarget = expandLocalPath(config.link_cli?.install_path || "$XDG_CONFIG_HOME/bin/lnk");
190204
const rcFile = expandLocalPath(config.shell_helper.rc_file);
191205
const registryPath = expandLocalPath(config.registry?.local_path || "~/.v8s.json");
192206
const repoPath = expandLocalPath(config.repository?.path || ROOT);
193207

194208
if (args.dryRun) {
195209
console.log(`[dry-run] would copy scripts/v8s.sh to ${helperTarget}`);
210+
console.log(`[dry-run] would copy scripts/lnk to ${lnkTarget}`);
196211
console.log(`[dry-run] would update ${rcFile}`);
197212
return;
198213
}
199214

200215
fs.mkdirSync(path.dirname(helperTarget), { recursive: true });
201216
fs.copyFileSync(HELPER_SOURCE_PATH, helperTarget);
217+
copyExecutable(LNK_SOURCE_PATH, lnkTarget);
202218

203219
const block = [
204220
START_MARKER,
@@ -218,9 +234,28 @@ function installHelper(config, args) {
218234

219235
fs.writeFileSync(rcFile, next);
220236
console.log(`Installed V8S shell helper to ${helperTarget}`);
237+
console.log(`Installed lnk CLI to ${lnkTarget}`);
221238
console.log(`Updated ${rcFile}`);
222239
}
223240

241+
function installLnkCli(config, args) {
242+
const lnkTarget = expandLocalPath(config.link_cli?.install_path || "$XDG_CONFIG_HOME/bin/lnk");
243+
244+
if (args.dryRun) {
245+
console.log(`[dry-run] would copy scripts/lnk to ${lnkTarget}`);
246+
return;
247+
}
248+
249+
copyExecutable(LNK_SOURCE_PATH, lnkTarget);
250+
console.log(`Installed lnk CLI to ${lnkTarget}`);
251+
}
252+
253+
function copyExecutable(sourcePath, targetPath) {
254+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
255+
fs.copyFileSync(sourcePath, targetPath);
256+
fs.chmodSync(targetPath, 0o755);
257+
}
258+
224259
function escapeRegExp(value) {
225260
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
226261
}

0 commit comments

Comments
 (0)