@dhruvwill/skills-cli 1.0.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.
@@ -0,0 +1,119 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { mkdir, rm, writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { hashDirectory, directoryExists, compareDirectories } from "../src/lib/hash.ts";
6
+
7
+ describe("Hash Utilities", () => {
8
+ let testDir: string;
9
+
10
+ beforeEach(async () => {
11
+ testDir = join(tmpdir(), `skills-hash-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
12
+ await mkdir(testDir, { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await rm(testDir, { recursive: true, force: true });
17
+ });
18
+
19
+ describe("directoryExists", () => {
20
+ test("returns true for existing directory", async () => {
21
+ expect(await directoryExists(testDir)).toBe(true);
22
+ });
23
+
24
+ test("returns false for non-existing directory", async () => {
25
+ expect(await directoryExists(join(testDir, "nonexistent"))).toBe(false);
26
+ });
27
+
28
+ test("returns false for file path", async () => {
29
+ const filePath = join(testDir, "file.txt");
30
+ await writeFile(filePath, "content");
31
+ expect(await directoryExists(filePath)).toBe(false);
32
+ });
33
+ });
34
+
35
+ describe("hashDirectory", () => {
36
+ test("returns 'empty' for empty directory", async () => {
37
+ const hash = await hashDirectory(testDir);
38
+ expect(hash).toBe("empty");
39
+ });
40
+
41
+ test("returns consistent hash for same content", async () => {
42
+ await writeFile(join(testDir, "file1.txt"), "content1");
43
+ await writeFile(join(testDir, "file2.txt"), "content2");
44
+
45
+ const hash1 = await hashDirectory(testDir);
46
+ const hash2 = await hashDirectory(testDir);
47
+
48
+ expect(hash1).toBe(hash2);
49
+ });
50
+
51
+ test("returns different hash for different content", async () => {
52
+ await writeFile(join(testDir, "file.txt"), "content1");
53
+ const hash1 = await hashDirectory(testDir);
54
+
55
+ await writeFile(join(testDir, "file.txt"), "content2");
56
+ const hash2 = await hashDirectory(testDir);
57
+
58
+ expect(hash1).not.toBe(hash2);
59
+ });
60
+
61
+ test("includes nested directories in hash", async () => {
62
+ await mkdir(join(testDir, "subdir"), { recursive: true });
63
+ await writeFile(join(testDir, "subdir", "nested.txt"), "nested content");
64
+
65
+ const hash1 = await hashDirectory(testDir);
66
+
67
+ await writeFile(join(testDir, "subdir", "nested.txt"), "modified content");
68
+ const hash2 = await hashDirectory(testDir);
69
+
70
+ expect(hash1).not.toBe(hash2);
71
+ });
72
+
73
+ test("returns 'empty' for non-existing directory", async () => {
74
+ const hash = await hashDirectory(join(testDir, "nonexistent"));
75
+ expect(hash).toBe("empty");
76
+ });
77
+ });
78
+
79
+ describe("compareDirectories", () => {
80
+ let sourceDir: string;
81
+ let targetDir: string;
82
+
83
+ beforeEach(async () => {
84
+ sourceDir = join(testDir, "source");
85
+ targetDir = join(testDir, "target");
86
+ await mkdir(sourceDir, { recursive: true });
87
+ await mkdir(targetDir, { recursive: true });
88
+ });
89
+
90
+ test("returns 'synced' for identical directories", async () => {
91
+ await writeFile(join(sourceDir, "file.txt"), "content");
92
+ await writeFile(join(targetDir, "file.txt"), "content");
93
+
94
+ const status = await compareDirectories(sourceDir, targetDir);
95
+ expect(status).toBe("synced");
96
+ });
97
+
98
+ test("returns 'not synced' for different directories", async () => {
99
+ await writeFile(join(sourceDir, "file.txt"), "content1");
100
+ await writeFile(join(targetDir, "file.txt"), "content2");
101
+
102
+ const status = await compareDirectories(sourceDir, targetDir);
103
+ expect(status).toBe("not synced");
104
+ });
105
+
106
+ test("returns 'target missing' for non-existing target", async () => {
107
+ await writeFile(join(sourceDir, "file.txt"), "content");
108
+ await rm(targetDir, { recursive: true });
109
+
110
+ const status = await compareDirectories(sourceDir, targetDir);
111
+ expect(status).toBe("target missing");
112
+ });
113
+
114
+ test("returns 'synced' for two empty directories", async () => {
115
+ const status = await compareDirectories(sourceDir, targetDir);
116
+ expect(status).toBe("synced");
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,173 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { parseGitUrl, getRemoteNamespace, getLocalNamespace } from "../src/lib/paths.ts";
3
+
4
+ describe("parseGitUrl", () => {
5
+ describe("GitHub URLs", () => {
6
+ test("parses basic GitHub HTTPS URL", () => {
7
+ const result = parseGitUrl("https://github.com/owner/repo");
8
+ expect(result).toEqual({
9
+ host: "github.com",
10
+ owner: "owner",
11
+ repo: "repo",
12
+ cloneUrl: "https://github.com/owner/repo.git",
13
+ });
14
+ });
15
+
16
+ test("parses GitHub URL with .git suffix", () => {
17
+ const result = parseGitUrl("https://github.com/owner/repo.git");
18
+ expect(result).toEqual({
19
+ host: "github.com",
20
+ owner: "owner",
21
+ repo: "repo",
22
+ cloneUrl: "https://github.com/owner/repo.git",
23
+ });
24
+ });
25
+
26
+ test("parses GitHub URL with tree subdirectory", () => {
27
+ const result = parseGitUrl("https://github.com/owner/repo/tree/main/path/to/skill");
28
+ expect(result).toEqual({
29
+ host: "github.com",
30
+ owner: "owner",
31
+ repo: "repo",
32
+ branch: "main",
33
+ subdir: "path/to/skill",
34
+ cloneUrl: "https://github.com/owner/repo.git",
35
+ });
36
+ });
37
+
38
+ test("parses GitHub URL with blob subdirectory", () => {
39
+ const result = parseGitUrl("https://github.com/owner/repo/blob/main/skills/my-skill");
40
+ expect(result).toEqual({
41
+ host: "github.com",
42
+ owner: "owner",
43
+ repo: "repo",
44
+ branch: "main",
45
+ subdir: "skills/my-skill",
46
+ cloneUrl: "https://github.com/owner/repo.git",
47
+ });
48
+ });
49
+
50
+ test("parses GitHub SSH URL", () => {
51
+ const result = parseGitUrl("git@github.com:owner/repo.git");
52
+ expect(result).toEqual({
53
+ host: "github.com",
54
+ owner: "owner",
55
+ repo: "repo",
56
+ cloneUrl: "git@github.com:owner/repo.git",
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("GitLab URLs", () => {
62
+ test("parses basic GitLab URL", () => {
63
+ const result = parseGitUrl("https://gitlab.com/owner/repo");
64
+ expect(result).toEqual({
65
+ host: "gitlab.com",
66
+ owner: "owner",
67
+ repo: "repo",
68
+ cloneUrl: "https://gitlab.com/owner/repo.git",
69
+ });
70
+ });
71
+
72
+ test("parses GitLab URL with subdirectory", () => {
73
+ const result = parseGitUrl("https://gitlab.com/owner/repo/-/tree/main/skills/my-skill");
74
+ expect(result).toEqual({
75
+ host: "gitlab.com",
76
+ owner: "owner",
77
+ repo: "repo",
78
+ branch: "main",
79
+ subdir: "skills/my-skill",
80
+ cloneUrl: "https://gitlab.com/owner/repo.git",
81
+ });
82
+ });
83
+
84
+ test("parses GitLab SSH URL", () => {
85
+ const result = parseGitUrl("git@gitlab.com:owner/repo.git");
86
+ expect(result).toEqual({
87
+ host: "gitlab.com",
88
+ owner: "owner",
89
+ repo: "repo",
90
+ cloneUrl: "git@gitlab.com:owner/repo.git",
91
+ });
92
+ });
93
+ });
94
+
95
+ describe("Bitbucket URLs", () => {
96
+ test("parses basic Bitbucket URL", () => {
97
+ const result = parseGitUrl("https://bitbucket.org/owner/repo");
98
+ expect(result).toEqual({
99
+ host: "bitbucket.org",
100
+ owner: "owner",
101
+ repo: "repo",
102
+ cloneUrl: "https://bitbucket.org/owner/repo.git",
103
+ });
104
+ });
105
+
106
+ test("parses Bitbucket URL with subdirectory", () => {
107
+ const result = parseGitUrl("https://bitbucket.org/owner/repo/src/main/skills/my-skill");
108
+ expect(result).toEqual({
109
+ host: "bitbucket.org",
110
+ owner: "owner",
111
+ repo: "repo",
112
+ branch: "main",
113
+ subdir: "skills/my-skill",
114
+ cloneUrl: "https://bitbucket.org/owner/repo.git",
115
+ });
116
+ });
117
+ });
118
+
119
+ describe("Generic Git URLs", () => {
120
+ test("parses self-hosted HTTPS URL", () => {
121
+ const result = parseGitUrl("https://git.company.com/owner/repo.git");
122
+ expect(result).toEqual({
123
+ host: "git.company.com",
124
+ owner: "owner",
125
+ repo: "repo",
126
+ cloneUrl: "https://git.company.com/owner/repo.git",
127
+ });
128
+ });
129
+
130
+ test("parses generic SSH URL", () => {
131
+ const result = parseGitUrl("git@git.company.com:owner/repo.git");
132
+ expect(result).toEqual({
133
+ host: "git.company.com",
134
+ owner: "owner",
135
+ repo: "repo",
136
+ cloneUrl: "git@git.company.com:owner/repo.git",
137
+ });
138
+ });
139
+ });
140
+
141
+ describe("Invalid URLs", () => {
142
+ test("returns null for invalid URL", () => {
143
+ expect(parseGitUrl("not-a-url")).toBeNull();
144
+ expect(parseGitUrl("")).toBeNull();
145
+ expect(parseGitUrl("ftp://example.com")).toBeNull();
146
+ });
147
+ });
148
+ });
149
+
150
+ describe("getRemoteNamespace", () => {
151
+ test("returns owner/repo for basic URL", () => {
152
+ const parsed = parseGitUrl("https://github.com/owner/repo")!;
153
+ expect(getRemoteNamespace(parsed)).toBe("owner/repo");
154
+ });
155
+
156
+ test("returns owner/skill-name for subdirectory URL", () => {
157
+ const parsed = parseGitUrl("https://github.com/owner/repo/tree/main/skills/my-skill")!;
158
+ expect(getRemoteNamespace(parsed)).toBe("owner/my-skill");
159
+ });
160
+
161
+ test("handles nested subdirectories", () => {
162
+ const parsed = parseGitUrl("https://github.com/owner/repo/tree/main/path/to/deep/skill")!;
163
+ expect(getRemoteNamespace(parsed)).toBe("owner/skill");
164
+ });
165
+ });
166
+
167
+ describe("getLocalNamespace", () => {
168
+ test("extracts folder name from path", () => {
169
+ expect(getLocalNamespace("/home/user/my-skills")).toBe("local/my-skills");
170
+ expect(getLocalNamespace("C:\\Users\\Admin\\skills")).toBe("local/skills");
171
+ expect(getLocalNamespace("./relative-path")).toBe("local/relative-path");
172
+ });
173
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false
28
+ }
29
+ }