vskill 0.1.0 → 0.1.2

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.
Files changed (112) hide show
  1. package/dist/agents/agents-registry.d.ts +48 -37
  2. package/dist/agents/agents-registry.js +206 -261
  3. package/dist/agents/agents-registry.js.map +1 -1
  4. package/dist/agents/agents-registry.test.d.ts +1 -0
  5. package/dist/agents/agents-registry.test.js +203 -0
  6. package/dist/agents/agents-registry.test.js.map +1 -0
  7. package/dist/api/client.js +9 -1
  8. package/dist/api/client.js.map +1 -1
  9. package/dist/api/client.test.d.ts +1 -0
  10. package/dist/api/client.test.js +204 -0
  11. package/dist/api/client.test.js.map +1 -0
  12. package/dist/blocklist/blocklist.d.ts +23 -0
  13. package/dist/blocklist/blocklist.js +116 -0
  14. package/dist/blocklist/blocklist.js.map +1 -0
  15. package/dist/blocklist/blocklist.test.d.ts +1 -0
  16. package/dist/blocklist/blocklist.test.js +259 -0
  17. package/dist/blocklist/blocklist.test.js.map +1 -0
  18. package/dist/blocklist/types.d.ts +18 -0
  19. package/dist/blocklist/types.js +5 -0
  20. package/dist/blocklist/types.js.map +1 -0
  21. package/dist/commands/add.d.ts +2 -0
  22. package/dist/commands/add.js +221 -6
  23. package/dist/commands/add.js.map +1 -1
  24. package/dist/commands/add.test.d.ts +1 -0
  25. package/dist/commands/add.test.js +682 -0
  26. package/dist/commands/add.test.js.map +1 -0
  27. package/dist/commands/blocklist.d.ts +1 -0
  28. package/dist/commands/blocklist.js +87 -0
  29. package/dist/commands/blocklist.js.map +1 -0
  30. package/dist/commands/blocklist.test.d.ts +1 -0
  31. package/dist/commands/blocklist.test.js +158 -0
  32. package/dist/commands/blocklist.test.js.map +1 -0
  33. package/dist/commands/remove.d.ts +7 -0
  34. package/dist/commands/remove.js +91 -0
  35. package/dist/commands/remove.js.map +1 -0
  36. package/dist/commands/remove.test.d.ts +1 -0
  37. package/dist/commands/remove.test.js +164 -0
  38. package/dist/commands/remove.test.js.map +1 -0
  39. package/dist/commands/submit.d.ts +1 -1
  40. package/dist/commands/submit.js +26 -16
  41. package/dist/commands/submit.js.map +1 -1
  42. package/dist/commands/submit.test.d.ts +1 -0
  43. package/dist/commands/submit.test.js +74 -0
  44. package/dist/commands/submit.test.js.map +1 -0
  45. package/dist/index.js +21 -2
  46. package/dist/index.js.map +1 -1
  47. package/dist/lockfile/lockfile.test.d.ts +1 -0
  48. package/dist/lockfile/lockfile.test.js +196 -0
  49. package/dist/lockfile/lockfile.test.js.map +1 -0
  50. package/dist/lockfile/types.d.ts +8 -0
  51. package/dist/marketplace/index.d.ts +2 -0
  52. package/dist/marketplace/index.js +2 -0
  53. package/dist/marketplace/index.js.map +1 -0
  54. package/dist/marketplace/marketplace.d.ts +34 -0
  55. package/dist/marketplace/marketplace.js +51 -0
  56. package/dist/marketplace/marketplace.js.map +1 -0
  57. package/dist/marketplace/marketplace.test.d.ts +1 -0
  58. package/dist/marketplace/marketplace.test.js +103 -0
  59. package/dist/marketplace/marketplace.test.js.map +1 -0
  60. package/dist/scanner/index.d.ts +4 -0
  61. package/dist/scanner/index.js +4 -0
  62. package/dist/scanner/index.js.map +1 -1
  63. package/dist/scanner/patterns.test.d.ts +1 -0
  64. package/dist/scanner/patterns.test.js +353 -0
  65. package/dist/scanner/patterns.test.js.map +1 -0
  66. package/dist/scanner/repo-scanner.d.ts +30 -0
  67. package/dist/scanner/repo-scanner.js +190 -0
  68. package/dist/scanner/repo-scanner.js.map +1 -0
  69. package/dist/scanner/tier1.test.d.ts +1 -0
  70. package/dist/scanner/tier1.test.js +182 -0
  71. package/dist/scanner/tier1.test.js.map +1 -0
  72. package/dist/scanner/tier2-llm.d.ts +28 -0
  73. package/dist/scanner/tier2-llm.js +130 -0
  74. package/dist/scanner/tier2-llm.js.map +1 -0
  75. package/dist/scanner/types.d.ts +58 -0
  76. package/dist/scanner/types.js +5 -0
  77. package/dist/scanner/types.js.map +1 -0
  78. package/dist/security/index.d.ts +2 -0
  79. package/dist/security/index.js +2 -0
  80. package/dist/security/index.js.map +1 -0
  81. package/dist/security/platform-security.d.ts +17 -0
  82. package/dist/security/platform-security.js +34 -0
  83. package/dist/security/platform-security.js.map +1 -0
  84. package/dist/security/platform-security.test.d.ts +1 -0
  85. package/dist/security/platform-security.test.js +92 -0
  86. package/dist/security/platform-security.test.js.map +1 -0
  87. package/dist/settings/index.d.ts +2 -0
  88. package/dist/settings/index.js +2 -0
  89. package/dist/settings/index.js.map +1 -0
  90. package/dist/settings/settings.d.ts +16 -0
  91. package/dist/settings/settings.js +73 -0
  92. package/dist/settings/settings.js.map +1 -0
  93. package/dist/settings/settings.test.d.ts +1 -0
  94. package/dist/settings/settings.test.js +103 -0
  95. package/dist/settings/settings.test.js.map +1 -0
  96. package/dist/utils/__tests__/paths.test.d.ts +1 -0
  97. package/dist/utils/__tests__/paths.test.js +22 -0
  98. package/dist/utils/__tests__/paths.test.js.map +1 -0
  99. package/dist/utils/__tests__/validation.test.d.ts +1 -0
  100. package/dist/utils/__tests__/validation.test.js +49 -0
  101. package/dist/utils/__tests__/validation.test.js.map +1 -0
  102. package/dist/utils/browser.d.ts +6 -0
  103. package/dist/utils/browser.js +37 -0
  104. package/dist/utils/browser.js.map +1 -0
  105. package/dist/utils/paths.d.ts +5 -0
  106. package/dist/utils/paths.js +13 -0
  107. package/dist/utils/paths.js.map +1 -0
  108. package/dist/utils/validation.d.ts +14 -0
  109. package/dist/utils/validation.js +34 -0
  110. package/dist/utils/validation.js.map +1 -0
  111. package/package.json +5 -2
  112. package/scripts/preuninstall.cjs +58 -0
@@ -0,0 +1,34 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Platform security check — queries external SAST scan results (best-effort)
3
+ // ---------------------------------------------------------------------------
4
+ const BASE_URL = "https://verified-skill.com";
5
+ /**
6
+ * Check external SAST scan results from the platform API.
7
+ * Returns null on any network/API error (best-effort, non-fatal).
8
+ */
9
+ export async function checkPlatformSecurity(skillName) {
10
+ try {
11
+ const url = `${BASE_URL}/api/v1/skills/${encodeURIComponent(skillName)}/security`;
12
+ const res = await fetch(url);
13
+ if (!res.ok)
14
+ return null;
15
+ const data = (await res.json());
16
+ const providers = (data.providers || []).map((p) => ({
17
+ provider: String(p.provider || ""),
18
+ status: String(p.status || "PENDING"),
19
+ verdict: p.verdict ?? null,
20
+ criticalCount: Number(p.criticalCount ?? 0),
21
+ }));
22
+ const hasCritical = providers.some((p) => p.status === "FAIL" && p.criticalCount > 0);
23
+ return {
24
+ hasCritical,
25
+ overallVerdict: String(data.overallVerdict || "PENDING"),
26
+ providers,
27
+ reportUrl: String(data.reportUrl || ""),
28
+ };
29
+ }
30
+ catch {
31
+ return null;
32
+ }
33
+ }
34
+ //# sourceMappingURL=platform-security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-security.js","sourceRoot":"","sources":["../../src/security/platform-security.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAE9E,MAAM,QAAQ,GAAG,4BAA4B,CAAC;AAgB9C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACzC,SAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,GAAG,QAAQ,kBAAkB,kBAAkB,CAAC,SAAS,CAAC,WAAW,CAAC;QAClF,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAE7B,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAS7B,CAAC;QAEF,MAAM,SAAS,GAAqB,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACrE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC;YAClC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,IAAI,SAAS,CAAC;YACrC,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,IAAI;YAC1B,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC;SAC5C,CAAC,CAAC,CAAC;QAEJ,MAAM,WAAW,GAAG,SAAS,CAAC,IAAI,CAChC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,aAAa,GAAG,CAAC,CAClD,CAAC;QAEF,OAAO;YACL,WAAW;YACX,cAAc,EAAE,MAAM,CAAC,IAAI,CAAC,cAAc,IAAI,SAAS,CAAC;YACxD,SAAS;YACT,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC;SACxC,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { checkPlatformSecurity } from "./platform-security.js";
3
+ // ---------------------------------------------------------------------------
4
+ // Mock global fetch
5
+ // ---------------------------------------------------------------------------
6
+ const originalFetch = globalThis.fetch;
7
+ beforeEach(() => {
8
+ vi.clearAllMocks();
9
+ });
10
+ afterEach(() => {
11
+ globalThis.fetch = originalFetch;
12
+ });
13
+ // ---------------------------------------------------------------------------
14
+ // Helpers
15
+ // ---------------------------------------------------------------------------
16
+ function makePlatformResponse(overrides = {}) {
17
+ return {
18
+ overallVerdict: "PASS",
19
+ reportUrl: "/skills/test-skill/security",
20
+ providers: [
21
+ {
22
+ provider: "semgrep",
23
+ status: "PASS",
24
+ verdict: "clean",
25
+ criticalCount: 0,
26
+ },
27
+ ],
28
+ ...overrides,
29
+ };
30
+ }
31
+ // ---------------------------------------------------------------------------
32
+ // Tests
33
+ // ---------------------------------------------------------------------------
34
+ describe("checkPlatformSecurity", () => {
35
+ it("returns hasCritical: true when a provider has FAIL + criticalCount > 0", async () => {
36
+ globalThis.fetch = vi.fn().mockResolvedValue({
37
+ ok: true,
38
+ json: async () => makePlatformResponse({
39
+ overallVerdict: "FAIL",
40
+ providers: [
41
+ { provider: "semgrep", status: "FAIL", verdict: "critical", criticalCount: 3 },
42
+ { provider: "snyk", status: "PASS", verdict: "clean", criticalCount: 0 },
43
+ ],
44
+ }),
45
+ });
46
+ const result = await checkPlatformSecurity("evil-skill");
47
+ expect(result).not.toBeNull();
48
+ expect(result.hasCritical).toBe(true);
49
+ expect(result.overallVerdict).toBe("FAIL");
50
+ expect(result.providers).toHaveLength(2);
51
+ expect(result.providers[0].criticalCount).toBe(3);
52
+ });
53
+ it("returns hasCritical: false when all providers PASS", async () => {
54
+ globalThis.fetch = vi.fn().mockResolvedValue({
55
+ ok: true,
56
+ json: async () => makePlatformResponse(),
57
+ });
58
+ const result = await checkPlatformSecurity("safe-skill");
59
+ expect(result).not.toBeNull();
60
+ expect(result.hasCritical).toBe(false);
61
+ expect(result.overallVerdict).toBe("PASS");
62
+ });
63
+ it("returns null on network error", async () => {
64
+ globalThis.fetch = vi.fn().mockRejectedValue(new Error("Network unreachable"));
65
+ const result = await checkPlatformSecurity("any-skill");
66
+ expect(result).toBeNull();
67
+ });
68
+ it("returns null on 404 response", async () => {
69
+ globalThis.fetch = vi.fn().mockResolvedValue({
70
+ ok: false,
71
+ status: 404,
72
+ });
73
+ const result = await checkPlatformSecurity("unknown-skill");
74
+ expect(result).toBeNull();
75
+ });
76
+ it("returns hasCritical: false when status is PENDING (does not block)", async () => {
77
+ globalThis.fetch = vi.fn().mockResolvedValue({
78
+ ok: true,
79
+ json: async () => makePlatformResponse({
80
+ overallVerdict: "PENDING",
81
+ providers: [
82
+ { provider: "semgrep", status: "PENDING", verdict: null, criticalCount: 0 },
83
+ ],
84
+ }),
85
+ });
86
+ const result = await checkPlatformSecurity("pending-skill");
87
+ expect(result).not.toBeNull();
88
+ expect(result.hasCritical).toBe(false);
89
+ expect(result.overallVerdict).toBe("PENDING");
90
+ });
91
+ });
92
+ //# sourceMappingURL=platform-security.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-security.test.js","sourceRoot":"","sources":["../../src/security/platform-security.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACzE,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E,MAAM,aAAa,GAAG,UAAU,CAAC,KAAK,CAAC;AAEvC,UAAU,CAAC,GAAG,EAAE;IACd,EAAE,CAAC,aAAa,EAAE,CAAC;AACrB,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,UAAU,CAAC,KAAK,GAAG,aAAa,CAAC;AACnC,CAAC,CAAC,CAAC;AAEH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,oBAAoB,CAAC,YAAqC,EAAE;IACnE,OAAO;QACL,cAAc,EAAE,MAAM;QACtB,SAAS,EAAE,6BAA6B;QACxC,SAAS,EAAE;YACT;gBACE,QAAQ,EAAE,SAAS;gBACnB,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,OAAO;gBAChB,aAAa,EAAE,CAAC;aACjB;SACF;QACD,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,QAAQ;AACR,8EAA8E;AAE9E,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;QACtF,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC3C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CACf,oBAAoB,CAAC;gBACnB,cAAc,EAAE,MAAM;gBACtB,SAAS,EAAE;oBACT,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,EAAE;oBAC9E,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE;iBACzE;aACF,CAAC;SACL,CAA4B,CAAC;QAE9B,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,YAAY,CAAC,CAAC;QAEzD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACvC,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,CAAC,MAAO,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC3C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,oBAAoB,EAAE;SACzC,CAA4B,CAAC;QAE9B,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,YAAY,CAAC,CAAC;QAEzD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,KAAK,IAAI,EAAE;QAC7C,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAC1C,IAAI,KAAK,CAAC,qBAAqB,CAAC,CACN,CAAC;QAE7B,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,WAAW,CAAC,CAAC;QAExD,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,KAAK,IAAI,EAAE;QAC5C,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC3C,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;SACZ,CAA4B,CAAC;QAE9B,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,CAAC;QAE5D,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,UAAU,CAAC,KAAK,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,iBAAiB,CAAC;YAC3C,EAAE,EAAE,IAAI;YACR,IAAI,EAAE,KAAK,IAAI,EAAE,CACf,oBAAoB,CAAC;gBACnB,cAAc,EAAE,SAAS;gBACzB,SAAS,EAAE;oBACT,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE;iBAC5E;aACF,CAAC;SACL,CAA4B,CAAC;QAE9B,MAAM,MAAM,GAAG,MAAM,qBAAqB,CAAC,eAAe,CAAC,CAAC;QAE5D,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACxC,MAAM,CAAC,MAAO,CAAC,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export type { SettingsOptions } from "./settings.js";
2
+ export { enablePlugin, disablePlugin, isPluginEnabled } from "./settings.js";
@@ -0,0 +1,2 @@
1
+ export { enablePlugin, disablePlugin, isPluginEnabled } from "./settings.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/settings/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface SettingsOptions {
2
+ scope: "user" | "project";
3
+ projectDir?: string;
4
+ }
5
+ /**
6
+ * Enable a plugin in settings.json.
7
+ */
8
+ export declare function enablePlugin(pluginId: string, opts: SettingsOptions): void;
9
+ /**
10
+ * Disable a plugin in settings.json (set to false).
11
+ */
12
+ export declare function disablePlugin(pluginId: string, opts: SettingsOptions): void;
13
+ /**
14
+ * Check if a plugin is enabled in settings.json.
15
+ */
16
+ export declare function isPluginEnabled(pluginId: string, opts: SettingsOptions): boolean;
@@ -0,0 +1,73 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Claude Code settings.json management
3
+ // ---------------------------------------------------------------------------
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+ /**
8
+ * Resolves the path to settings.json based on scope.
9
+ * - user: ~/.claude/settings.json
10
+ * - project: <projectDir>/.claude/settings.json
11
+ */
12
+ function settingsPath(opts) {
13
+ if (opts.scope === "project" && opts.projectDir) {
14
+ return join(opts.projectDir, ".claude", "settings.json");
15
+ }
16
+ return join(homedir(), ".claude", "settings.json");
17
+ }
18
+ /**
19
+ * Reads settings.json, returning an empty object if it doesn't exist.
20
+ */
21
+ function readSettings(opts) {
22
+ const p = settingsPath(opts);
23
+ if (!existsSync(p))
24
+ return {};
25
+ try {
26
+ const raw = readFileSync(p, "utf-8");
27
+ return JSON.parse(raw);
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ /**
34
+ * Writes settings.json, creating parent directories if needed.
35
+ */
36
+ function writeSettings(settings, opts) {
37
+ const p = settingsPath(opts);
38
+ const dir = dirname(p);
39
+ if (!existsSync(dir)) {
40
+ mkdirSync(dir, { recursive: true });
41
+ }
42
+ writeFileSync(p, JSON.stringify(settings, null, 2) + "\n", "utf-8");
43
+ }
44
+ /**
45
+ * Enable a plugin in settings.json.
46
+ */
47
+ export function enablePlugin(pluginId, opts) {
48
+ const settings = readSettings(opts);
49
+ if (!settings.enabledPlugins) {
50
+ settings.enabledPlugins = {};
51
+ }
52
+ settings.enabledPlugins[pluginId] = true;
53
+ writeSettings(settings, opts);
54
+ }
55
+ /**
56
+ * Disable a plugin in settings.json (set to false).
57
+ */
58
+ export function disablePlugin(pluginId, opts) {
59
+ const settings = readSettings(opts);
60
+ if (!settings.enabledPlugins) {
61
+ settings.enabledPlugins = {};
62
+ }
63
+ settings.enabledPlugins[pluginId] = false;
64
+ writeSettings(settings, opts);
65
+ }
66
+ /**
67
+ * Check if a plugin is enabled in settings.json.
68
+ */
69
+ export function isPluginEnabled(pluginId, opts) {
70
+ const settings = readSettings(opts);
71
+ return settings.enabledPlugins?.[pluginId] === true;
72
+ }
73
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/settings/settings.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,uCAAuC;AACvC,8EAA8E;AAE9E,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAYlC;;;;GAIG;AACH,SAAS,YAAY,CAAC,IAAqB;IACzC,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;QAChD,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AACrD,CAAC;AAED;;GAEG;AACH,SAAS,YAAY,CAAC,IAAqB;IACzC,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC7B,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAS,aAAa,CAAC,QAAsB,EAAE,IAAqB;IAClE,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAC7B,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACvB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AACtE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,QAAgB,EAChB,IAAqB;IAErB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC7B,QAAQ,CAAC,cAAc,GAAG,EAAE,CAAC;IAC/B,CAAC;IACD,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IACzC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAgB,EAChB,IAAqB;IAErB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC;QAC7B,QAAQ,CAAC,cAAc,GAAG,EAAE,CAAC;IAC/B,CAAC;IACD,QAAQ,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC;IAC1C,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAgB,EAChB,IAAqB;IAErB,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACpC,OAAO,QAAQ,CAAC,cAAc,EAAE,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;AACtD,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,103 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ // ---------------------------------------------------------------------------
3
+ // Mock node:fs
4
+ // ---------------------------------------------------------------------------
5
+ const mockReadFileSync = vi.fn();
6
+ const mockWriteFileSync = vi.fn();
7
+ const mockExistsSync = vi.fn();
8
+ const mockMkdirSync = vi.fn();
9
+ vi.mock("node:fs", () => ({
10
+ readFileSync: (...args) => mockReadFileSync(...args),
11
+ writeFileSync: (...args) => mockWriteFileSync(...args),
12
+ existsSync: (...args) => mockExistsSync(...args),
13
+ mkdirSync: (...args) => mockMkdirSync(...args),
14
+ }));
15
+ // ---------------------------------------------------------------------------
16
+ // Mock node:os (homedir)
17
+ // ---------------------------------------------------------------------------
18
+ vi.mock("node:os", () => ({
19
+ homedir: () => "/home/testuser",
20
+ }));
21
+ // Import after mocks are set up
22
+ const { enablePlugin, disablePlugin, isPluginEnabled } = await import("./settings.js");
23
+ // ---------------------------------------------------------------------------
24
+ // Tests
25
+ // ---------------------------------------------------------------------------
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ });
29
+ describe("enablePlugin", () => {
30
+ it("TC-010: enables plugin in empty/new settings.json", () => {
31
+ // settings.json doesn't exist
32
+ mockExistsSync.mockReturnValue(false);
33
+ enablePlugin("sw-frontend@specweave", { scope: "user" });
34
+ // Should write to ~/.claude/settings.json
35
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
36
+ const [writePath, writtenContent] = mockWriteFileSync.mock.calls[0];
37
+ expect(writePath).toBe("/home/testuser/.claude/settings.json");
38
+ const parsed = JSON.parse(writtenContent);
39
+ expect(parsed).toEqual({
40
+ enabledPlugins: { "sw-frontend@specweave": true },
41
+ });
42
+ });
43
+ it("TC-011: project scope writes to correct location", () => {
44
+ mockExistsSync.mockReturnValue(false);
45
+ enablePlugin("sw-frontend@specweave", {
46
+ scope: "project",
47
+ projectDir: "/tmp/test-project",
48
+ });
49
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
50
+ const [writePath] = mockWriteFileSync.mock.calls[0];
51
+ expect(writePath).toBe("/tmp/test-project/.claude/settings.json");
52
+ });
53
+ it("TC-012: preserves existing settings when adding plugin", () => {
54
+ mockExistsSync.mockReturnValue(true);
55
+ mockReadFileSync.mockReturnValue(JSON.stringify({
56
+ theme: "dark",
57
+ enabledPlugins: { other: true },
58
+ }));
59
+ enablePlugin("sw-frontend@specweave", { scope: "user" });
60
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
61
+ const [, writtenContent] = mockWriteFileSync.mock.calls[0];
62
+ const parsed = JSON.parse(writtenContent);
63
+ expect(parsed).toEqual({
64
+ theme: "dark",
65
+ enabledPlugins: {
66
+ other: true,
67
+ "sw-frontend@specweave": true,
68
+ },
69
+ });
70
+ });
71
+ });
72
+ describe("disablePlugin", () => {
73
+ it("sets plugin to false in settings.json", () => {
74
+ mockExistsSync.mockReturnValue(true);
75
+ mockReadFileSync.mockReturnValue(JSON.stringify({
76
+ enabledPlugins: { "sw-frontend@specweave": true },
77
+ }));
78
+ disablePlugin("sw-frontend@specweave", { scope: "user" });
79
+ expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
80
+ const [, writtenContent] = mockWriteFileSync.mock.calls[0];
81
+ const parsed = JSON.parse(writtenContent);
82
+ expect(parsed.enabledPlugins["sw-frontend@specweave"]).toBe(false);
83
+ });
84
+ });
85
+ describe("isPluginEnabled", () => {
86
+ it("returns true when plugin is enabled", () => {
87
+ mockExistsSync.mockReturnValue(true);
88
+ mockReadFileSync.mockReturnValue(JSON.stringify({
89
+ enabledPlugins: { "sw-frontend@specweave": true },
90
+ }));
91
+ expect(isPluginEnabled("sw-frontend@specweave", { scope: "user" })).toBe(true);
92
+ });
93
+ it("returns false when settings.json does not exist", () => {
94
+ mockExistsSync.mockReturnValue(false);
95
+ expect(isPluginEnabled("sw-frontend@specweave", { scope: "user" })).toBe(false);
96
+ });
97
+ it("returns false when plugin is not in settings", () => {
98
+ mockExistsSync.mockReturnValue(true);
99
+ mockReadFileSync.mockReturnValue(JSON.stringify({ enabledPlugins: {} }));
100
+ expect(isPluginEnabled("sw-frontend@specweave", { scope: "user" })).toBe(false);
101
+ });
102
+ });
103
+ //# sourceMappingURL=settings.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.test.js","sourceRoot":"","sources":["../../src/settings/settings.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import os from "node:os";
3
+ import { resolveTilde } from "../paths.js";
4
+ describe("resolveTilde (T-004)", () => {
5
+ const home = os.homedir();
6
+ it("TC-009: resolves ~ to home directory", () => {
7
+ expect(resolveTilde("~/some/path")).toBe(`${home}/some/path`);
8
+ });
9
+ it("TC-010: passes through absolute paths unchanged", () => {
10
+ expect(resolveTilde("/usr/local/bin")).toBe("/usr/local/bin");
11
+ });
12
+ it("TC-011: passes through relative paths unchanged", () => {
13
+ expect(resolveTilde("./local/dir")).toBe("./local/dir");
14
+ });
15
+ it("resolves bare ~ to home directory", () => {
16
+ expect(resolveTilde("~")).toBe(home);
17
+ });
18
+ it("does not resolve tilde in middle of path", () => {
19
+ expect(resolveTilde("/some/~/path")).toBe("/some/~/path");
20
+ });
21
+ });
22
+ //# sourceMappingURL=paths.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/paths.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;IAE1B,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,YAAY,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { validateRepoSegment, validateSkillName } from "../validation.js";
3
+ describe("validateRepoSegment (T-011)", () => {
4
+ it("TC-023: accepts valid owner/repo names", () => {
5
+ expect(validateRepoSegment("my-org")).toBe(true);
6
+ expect(validateRepoSegment("user.name")).toBe(true);
7
+ expect(validateRepoSegment("repo_123")).toBe(true);
8
+ expect(validateRepoSegment("A-Z_test.repo")).toBe(true);
9
+ });
10
+ it("TC-024: rejects path traversal", () => {
11
+ expect(validateRepoSegment("../etc/passwd")).toBe(false);
12
+ expect(validateRepoSegment("..")).toBe(false);
13
+ expect(validateRepoSegment("foo/../bar")).toBe(false);
14
+ });
15
+ it("TC-025: rejects null bytes", () => {
16
+ expect(validateRepoSegment("repo\x00name")).toBe(false);
17
+ });
18
+ it("rejects slashes", () => {
19
+ expect(validateRepoSegment("owner/repo")).toBe(false);
20
+ });
21
+ it("rejects empty string", () => {
22
+ expect(validateRepoSegment("")).toBe(false);
23
+ });
24
+ it("rejects spaces", () => {
25
+ expect(validateRepoSegment("my repo")).toBe(false);
26
+ });
27
+ });
28
+ describe("validateSkillName (T-011)", () => {
29
+ it("TC-026: accepts valid skill names", () => {
30
+ expect(validateSkillName("my-skill")).toBe(true);
31
+ expect(validateSkillName("skill_v2")).toBe(true);
32
+ expect(validateSkillName("code-review")).toBe(true);
33
+ });
34
+ it("TC-027: rejects path traversal", () => {
35
+ expect(validateSkillName("../../malicious")).toBe(false);
36
+ expect(validateSkillName("../..")).toBe(false);
37
+ expect(validateSkillName("skill/../../etc")).toBe(false);
38
+ });
39
+ it("rejects backslash traversal", () => {
40
+ expect(validateSkillName("..\\..\\malicious")).toBe(false);
41
+ });
42
+ it("rejects null bytes", () => {
43
+ expect(validateSkillName("skill\x00name")).toBe(false);
44
+ });
45
+ it("rejects empty string", () => {
46
+ expect(validateSkillName("")).toBe(false);
47
+ });
48
+ });
49
+ //# sourceMappingURL=validation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.test.js","sourceRoot":"","sources":["../../../src/utils/__tests__/validation.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Open a URL in the user's default browser.
3
+ * Uses execFile (not exec) to avoid shell command injection.
4
+ * Platform-specific: open (macOS), xdg-open (Linux), start (Windows).
5
+ */
6
+ export declare function openBrowser(url: string): Promise<void>;
@@ -0,0 +1,37 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Cross-platform browser opener
3
+ // ---------------------------------------------------------------------------
4
+ import { execFile } from "node:child_process";
5
+ /**
6
+ * Open a URL in the user's default browser.
7
+ * Uses execFile (not exec) to avoid shell command injection.
8
+ * Platform-specific: open (macOS), xdg-open (Linux), start (Windows).
9
+ */
10
+ export function openBrowser(url) {
11
+ const platform = process.platform;
12
+ let cmd;
13
+ let args;
14
+ if (platform === "darwin") {
15
+ cmd = "open";
16
+ args = [url];
17
+ }
18
+ else if (platform === "win32") {
19
+ cmd = "cmd";
20
+ args = ["/c", "start", "", url];
21
+ }
22
+ else {
23
+ cmd = "xdg-open";
24
+ args = [url];
25
+ }
26
+ return new Promise((resolve, reject) => {
27
+ execFile(cmd, args, (err) => {
28
+ if (err) {
29
+ reject(err);
30
+ }
31
+ else {
32
+ resolve();
33
+ }
34
+ });
35
+ });
36
+ }
37
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../../src/utils/browser.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAE9E,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC;IAClC,IAAI,GAAW,CAAC;IAChB,IAAI,IAAc,CAAC;IAEnB,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC1B,GAAG,GAAG,MAAM,CAAC;QACb,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;SAAM,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChC,GAAG,GAAG,KAAK,CAAC;QACZ,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,GAAG,CAAC,CAAC;IAClC,CAAC;SAAM,CAAC;QACN,GAAG,GAAG,UAAU,CAAC;QACjB,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACf,CAAC;IAED,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE;YAC1B,IAAI,GAAG,EAAE,CAAC;gBACR,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC;iBAAM,CAAC;gBACN,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resolve leading `~` in a path to the user's home directory.
3
+ * Passes through absolute and relative paths unchanged.
4
+ */
5
+ export declare function resolveTilde(p: string): string;
@@ -0,0 +1,13 @@
1
+ import os from "node:os";
2
+ /**
3
+ * Resolve leading `~` in a path to the user's home directory.
4
+ * Passes through absolute and relative paths unchanged.
5
+ */
6
+ export function resolveTilde(p) {
7
+ if (p === "~")
8
+ return os.homedir();
9
+ if (p.startsWith("~/"))
10
+ return os.homedir() + p.slice(1);
11
+ return p;
12
+ }
13
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/utils/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAEzB;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,CAAS;IACpC,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;IACnC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACzD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Input validation utilities for CLI arguments.
3
+ * Prevents path traversal and injection attacks.
4
+ */
5
+ /**
6
+ * Validate an owner or repo name segment.
7
+ * Only allows word chars, dots, and hyphens.
8
+ */
9
+ export declare function validateRepoSegment(segment: string): boolean;
10
+ /**
11
+ * Validate a skill name.
12
+ * Rejects path traversal (../, ..\) and null bytes.
13
+ */
14
+ export declare function validateSkillName(name: string): boolean;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Input validation utilities for CLI arguments.
3
+ * Prevents path traversal and injection attacks.
4
+ */
5
+ const REPO_SEGMENT_RE = /^[\w.-]+$/;
6
+ /**
7
+ * Validate an owner or repo name segment.
8
+ * Only allows word chars, dots, and hyphens.
9
+ */
10
+ export function validateRepoSegment(segment) {
11
+ if (!segment)
12
+ return false;
13
+ if (segment.includes("\0"))
14
+ return false;
15
+ if (segment === "." || segment === "..")
16
+ return false;
17
+ return REPO_SEGMENT_RE.test(segment);
18
+ }
19
+ /**
20
+ * Validate a skill name.
21
+ * Rejects path traversal (../, ..\) and null bytes.
22
+ */
23
+ export function validateSkillName(name) {
24
+ if (!name)
25
+ return false;
26
+ if (name.includes("\0"))
27
+ return false;
28
+ if (name.includes("../") || name.includes("..\\"))
29
+ return false;
30
+ if (name === ".." || name === ".")
31
+ return false;
32
+ return true;
33
+ }
34
+ //# sourceMappingURL=validation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,eAAe,GAAG,WAAW,CAAC;AAEpC;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe;IACjD,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IACtD,OAAO,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACvC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IAChE,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,KAAK,CAAC;IAChD,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vskill",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Secure multi-platform AI skill installer — scan before you install",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "dev": "tsc --watch",
14
14
  "test": "vitest run",
15
15
  "clean": "rm -rf dist",
16
+ "preuninstall": "node scripts/preuninstall.cjs",
16
17
  "prepublishOnly": "npm run build"
17
18
  },
18
19
  "keywords": [
@@ -35,6 +36,7 @@
35
36
  "homepage": "https://verified-skill.com",
36
37
  "files": [
37
38
  "dist",
39
+ "scripts/preuninstall.cjs",
38
40
  "README.md"
39
41
  ],
40
42
  "devDependencies": {
@@ -46,6 +48,7 @@
46
48
  "node": ">=20.0.0"
47
49
  },
48
50
  "dependencies": {
49
- "commander": "^14.0.3"
51
+ "commander": "^14.0.3",
52
+ "simple-git": "^3.27.0"
50
53
  }
51
54
  }