@thesmurph/agentlink 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/dist/scope.js ADDED
@@ -0,0 +1,100 @@
1
+ import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ export function resolveScope(scope, cwd) {
5
+ const root = scope === "global" ? homedir() : (findRepoRoot(cwd) ?? cwd);
6
+ const agentsDir = path.join(root, ".agents");
7
+ return {
8
+ scope,
9
+ root,
10
+ agentsDir,
11
+ instructions: path.join(root, "AGENTS.md"),
12
+ skills: path.join(agentsDir, "skills"),
13
+ stateFile: path.join(agentsDir, "agentlink.json"),
14
+ };
15
+ }
16
+ /**
17
+ * Walk up for a `.git` entry (directory, or a file for linked worktrees).
18
+ * Returns null when no repository root exists above cwd.
19
+ */
20
+ export function findRepoRoot(cwd) {
21
+ let dir = path.resolve(cwd);
22
+ for (;;) {
23
+ if (existsSync(path.join(dir, ".git")))
24
+ return dir;
25
+ const parent = path.dirname(dir);
26
+ if (parent === dir)
27
+ return null;
28
+ dir = parent;
29
+ }
30
+ }
31
+ export function expandHome(input) {
32
+ if (input === "~")
33
+ return homedir();
34
+ if (input.startsWith("~/"))
35
+ return path.join(homedir(), input.slice(2));
36
+ return input;
37
+ }
38
+ export function exists(target) {
39
+ try {
40
+ statSync(target);
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ export function isDirectory(target) {
48
+ try {
49
+ return statSync(target).isDirectory();
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /** Names of the immediate subdirectories of `dir`, sorted; `[]` when missing. */
56
+ export function listSubdirectories(dir) {
57
+ let entries;
58
+ try {
59
+ entries = readdirSync(dir, { withFileTypes: true });
60
+ }
61
+ catch {
62
+ return [];
63
+ }
64
+ const names = [];
65
+ for (const entry of entries) {
66
+ if (entry.name.startsWith("."))
67
+ continue;
68
+ if (entry.isDirectory()) {
69
+ names.push(entry.name);
70
+ }
71
+ else if (entry.isSymbolicLink()) {
72
+ // A symlink is only a skill if it resolves to a directory. A dangling or
73
+ // file-valued link would otherwise become a broken link in every harness.
74
+ try {
75
+ if (statSync(path.join(dir, entry.name)).isDirectory())
76
+ names.push(entry.name);
77
+ }
78
+ catch {
79
+ /* broken symlink: reported by doctor, never linked */
80
+ }
81
+ }
82
+ }
83
+ return names.sort();
84
+ }
85
+ /** Names of dot-directories, which the convention reserves and skips. */
86
+ export function listHiddenSubdirectories(dir) {
87
+ try {
88
+ return readdirSync(dir, { withFileTypes: true })
89
+ .filter((entry) => entry.name.startsWith(".") && (entry.isDirectory() || entry.isSymbolicLink()))
90
+ .map((entry) => entry.name)
91
+ .sort();
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ export function relativeTo(fromDir, target) {
98
+ const rel = path.relative(fromDir, target);
99
+ return rel === "" ? "." : rel;
100
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,108 @@
1
+ import { emitKeypressEvents } from "node:readline";
2
+ const ESC = String.fromCharCode(27);
3
+ const CURSOR_UP = (n) => (n > 0 ? `${ESC}[${n}A` : "");
4
+ const CLEAR_LINE = `${ESC}[2K`;
5
+ const DIM = `${ESC}[2m`;
6
+ const BOLD = `${ESC}[1m`;
7
+ const RESET = `${ESC}[0m`;
8
+ const CYAN = `${ESC}[36m`;
9
+ export async function selectMany(choices, options) {
10
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
11
+ return choices.filter((c) => c.checked).map((c) => c.id);
12
+ }
13
+ return new Promise((resolve) => {
14
+ emitKeypressEvents(process.stdin);
15
+ let cursor = 0;
16
+ let drawnLines = 0;
17
+ const lines = () => {
18
+ const out = [];
19
+ out.push(`${BOLD}${options.title}${RESET}`);
20
+ if (options.help)
21
+ out.push(`${DIM}${options.help}${RESET}`);
22
+ choices.forEach((choice, index) => {
23
+ const active = index === cursor;
24
+ const box = choice.checked ? "[x]" : "[ ]";
25
+ const pointer = active ? `${CYAN}>${RESET}` : " ";
26
+ const hint = choice.hint ? ` ${DIM}${choice.hint}${RESET}` : "";
27
+ const label = active ? `${BOLD}${choice.label}${RESET}` : choice.label;
28
+ out.push(`${pointer} ${box} ${label}${hint}`);
29
+ });
30
+ return out;
31
+ };
32
+ const draw = () => {
33
+ const rendered = lines();
34
+ let output = CURSOR_UP(drawnLines);
35
+ for (const line of rendered)
36
+ output += `${CLEAR_LINE}${line}\n`;
37
+ if (rendered.length < drawnLines) {
38
+ for (let i = rendered.length; i < drawnLines; i += 1)
39
+ output += `${CLEAR_LINE}\n`;
40
+ output += CURSOR_UP(drawnLines - rendered.length);
41
+ }
42
+ drawnLines = rendered.length;
43
+ process.stdout.write(output);
44
+ };
45
+ const cleanup = (result) => {
46
+ process.stdin.removeListener("keypress", onKey);
47
+ process.stdin.setRawMode?.(false);
48
+ process.stdin.pause();
49
+ resolve(result);
50
+ };
51
+ const onKey = (_str, key) => {
52
+ if (!key)
53
+ return;
54
+ const name = key.name ?? "";
55
+ if (key.ctrl && name === "c") {
56
+ process.stdout.write("\n");
57
+ cleanup(null);
58
+ return;
59
+ }
60
+ if (name === "escape") {
61
+ cleanup(null);
62
+ return;
63
+ }
64
+ if (name === "return" || name === "enter") {
65
+ process.stdout.write("\n");
66
+ cleanup(choices.filter((c) => c.checked).map((c) => c.id));
67
+ return;
68
+ }
69
+ if (name === "up" || name === "k") {
70
+ cursor = (cursor + choices.length - 1) % choices.length;
71
+ draw();
72
+ return;
73
+ }
74
+ if (name === "down" || name === "j") {
75
+ cursor = (cursor + 1) % choices.length;
76
+ draw();
77
+ return;
78
+ }
79
+ if (name === "space") {
80
+ const choice = choices[cursor];
81
+ if (choice)
82
+ choice.checked = !choice.checked;
83
+ draw();
84
+ return;
85
+ }
86
+ if (name === "a") {
87
+ const allOn = choices.every((c) => c.checked);
88
+ choices.forEach((c) => {
89
+ c.checked = !allOn;
90
+ });
91
+ draw();
92
+ return;
93
+ }
94
+ if (name === "i") {
95
+ choices.forEach((c) => {
96
+ c.checked = c.group === "detected" ? true : false;
97
+ });
98
+ draw();
99
+ return;
100
+ }
101
+ };
102
+ emitKeypressEvents(process.stdin);
103
+ process.stdin.setRawMode?.(true);
104
+ process.stdin.resume();
105
+ process.stdin.on("keypress", onKey);
106
+ draw();
107
+ });
108
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@thesmurph/agentlink",
3
+ "version": "0.1.0",
4
+ "description": "One source of truth for agent instructions and skills, symlinked into every coding-agent harness.",
5
+ "keywords": [
6
+ "agents.md",
7
+ "agent-skills",
8
+ "claude-code",
9
+ "codex",
10
+ "cursor",
11
+ "symlink",
12
+ "cli"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "Sean Murphy (https://github.com/smurphnerd)",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/smurphnerd/agentlink.git"
19
+ },
20
+ "homepage": "https://github.com/smurphnerd/agentlink#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/smurphnerd/agentlink/issues"
23
+ },
24
+ "type": "module",
25
+ "bin": {
26
+ "agentlink": "dist/cli.js"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "src",
31
+ "README.md",
32
+ "CONVENTION.md",
33
+ "CHANGELOG.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc",
40
+ "prepare": "tsc",
41
+ "dev": "node --experimental-strip-types src/cli.ts",
42
+ "test": "npm run build && node --test",
43
+ "test:unit": "node --test",
44
+ "typecheck": "tsc --noEmit",
45
+ "prepublishOnly": "npm test"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "devDependencies": {
51
+ "typescript": "^5.6.0",
52
+ "@types/node": "^22.0.0"
53
+ }
54
+ }