skillrepo 1.5.0 → 1.6.1
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/package.json +1 -1
- package/src/lib/fs-utils.mjs +10 -1
- package/src/lib/mergers/hooks-json.mjs +44 -8
- package/src/lib/paths.mjs +3 -0
- package/src/lib/write-configs.mjs +46 -6
- package/src/test/e2e/HANDOFF.md +244 -0
- package/src/test/e2e/cli-init.test.mjs +1230 -0
- package/src/test/e2e/mock-server.mjs +133 -0
- package/src/test/e2e/payload-factory.mjs +900 -0
- package/src/test/mergers/hooks-json.test.mjs +50 -11
package/package.json
CHANGED
package/src/lib/fs-utils.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Creates directories as needed, handles errors cleanly.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, chmodSync } from "node:fs";
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -35,6 +35,15 @@ export function writeFileSafe(filePath, content) {
|
|
|
35
35
|
writeFileSync(filePath, content, "utf-8");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Write a file and mark it executable (0o755).
|
|
40
|
+
* Used for hook scripts that have shebangs and are invoked directly.
|
|
41
|
+
*/
|
|
42
|
+
export function writeExecutable(filePath, content) {
|
|
43
|
+
writeFileSafe(filePath, content);
|
|
44
|
+
chmodSync(filePath, 0o755);
|
|
45
|
+
}
|
|
46
|
+
|
|
38
47
|
/**
|
|
39
48
|
* Check if a path exists (file or directory).
|
|
40
49
|
*/
|
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { unlinkSync } from "node:fs";
|
|
11
|
-
import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
|
|
11
|
+
import { readFileSafe, writeFileSafe, writeExecutable } from "../fs-utils.mjs";
|
|
12
12
|
import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudePreToolHook, claudeHooksDir } from "../paths.mjs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
|
|
15
|
-
const DEFAULT_SYNC_COMMAND = "
|
|
16
|
-
const DEFAULT_PROMPT_COMMAND = "
|
|
17
|
-
const DEFAULT_PRETOOL_COMMAND = "
|
|
15
|
+
const DEFAULT_SYNC_COMMAND = ".claude/hooks/skillrepo-sync.mjs";
|
|
16
|
+
const DEFAULT_PROMPT_COMMAND = ".claude/hooks/skillrepo-prompt-match.mjs";
|
|
17
|
+
const DEFAULT_PRETOOL_COMMAND = ".claude/hooks/skillrepo-pretool-match.mjs";
|
|
18
|
+
|
|
19
|
+
// Legacy commands used before v1.6.1 — recognized during migration so
|
|
20
|
+
// re-running init replaces them rather than duplicating.
|
|
21
|
+
const LEGACY_COMMANDS = new Set([
|
|
22
|
+
"node .claude/hooks/skillrepo-sync.mjs",
|
|
23
|
+
"node .claude/hooks/skillrepo-prompt-match.mjs",
|
|
24
|
+
"node .claude/hooks/skillrepo-pretool-match.mjs",
|
|
25
|
+
]);
|
|
18
26
|
|
|
19
27
|
/**
|
|
20
28
|
* Extract a hook command from a hooks config group, falling back to a default.
|
|
@@ -46,6 +54,26 @@ function hasCommand(groups, command) {
|
|
|
46
54
|
);
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Replace legacy `node .claude/hooks/...` commands with the new direct-execution
|
|
59
|
+
* format. Returns true if any replacement was made.
|
|
60
|
+
*/
|
|
61
|
+
function replaceLegacyCommands(groups) {
|
|
62
|
+
if (!Array.isArray(groups)) return false;
|
|
63
|
+
let replaced = false;
|
|
64
|
+
for (const group of groups) {
|
|
65
|
+
if (!Array.isArray(group.hooks)) continue;
|
|
66
|
+
for (const hook of group.hooks) {
|
|
67
|
+
if (LEGACY_COMMANDS.has(hook.command)) {
|
|
68
|
+
// Strip the "node " prefix → ".claude/hooks/..."
|
|
69
|
+
hook.command = hook.command.replace(/^node /, "");
|
|
70
|
+
replaced = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return replaced;
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
/**
|
|
50
78
|
* Merge SkillRepo hooks into .claude/settings.local.json and write hook scripts.
|
|
51
79
|
* @param {object} hooksConfig - The hooks config object from the server payload (settingsHooks.hooks)
|
|
@@ -60,16 +88,17 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
60
88
|
const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
|
|
61
89
|
const results = [];
|
|
62
90
|
|
|
63
|
-
// Always write hook scripts (latest version from server)
|
|
91
|
+
// Always write hook scripts (latest version from server) and mark executable.
|
|
92
|
+
// Scripts have #!/usr/bin/env node shebangs and are invoked directly (not via `node`).
|
|
64
93
|
const syncExisted = readFileSafe(claudeSyncHook()) !== null;
|
|
65
|
-
|
|
94
|
+
writeExecutable(claudeSyncHook(), syncHookContent);
|
|
66
95
|
results.push({
|
|
67
96
|
path: ".claude/hooks/skillrepo-sync.mjs",
|
|
68
97
|
action: syncExisted ? "updated" : "created",
|
|
69
98
|
});
|
|
70
99
|
|
|
71
100
|
const promptExisted = readFileSafe(claudePromptHook()) !== null;
|
|
72
|
-
|
|
101
|
+
writeExecutable(claudePromptHook(), promptHookContent);
|
|
73
102
|
results.push({
|
|
74
103
|
path: ".claude/hooks/skillrepo-prompt-match.mjs",
|
|
75
104
|
action: promptExisted ? "updated" : "created",
|
|
@@ -77,7 +106,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
77
106
|
|
|
78
107
|
if (preToolHookContent) {
|
|
79
108
|
const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
|
|
80
|
-
|
|
109
|
+
writeExecutable(claudePreToolHook(), preToolHookContent);
|
|
81
110
|
results.push({
|
|
82
111
|
path: ".claude/hooks/skillrepo-pretool-match.mjs",
|
|
83
112
|
action: preToolExisted ? "updated" : "created",
|
|
@@ -111,6 +140,13 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
|
|
|
111
140
|
if (!config.hooks) config.hooks = {};
|
|
112
141
|
let changed = false;
|
|
113
142
|
|
|
143
|
+
// Migrate legacy "node .claude/hooks/..." commands to direct execution
|
|
144
|
+
for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse"]) {
|
|
145
|
+
if (Array.isArray(config.hooks[event]) && replaceLegacyCommands(config.hooks[event])) {
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
114
150
|
// Merge SessionStart
|
|
115
151
|
if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
|
|
116
152
|
if (!hasCommand(config.hooks.SessionStart, syncCommand)) {
|
package/src/lib/paths.mjs
CHANGED
|
@@ -24,7 +24,10 @@ export const claudeSkillrepoConfig = () => join(cwd(), ".claude", "skillrepo-con
|
|
|
24
24
|
export const cursorDir = () => join(cwd(), ".cursor");
|
|
25
25
|
export const cursorMcpJson = () => join(cwd(), ".cursor", "mcp.json");
|
|
26
26
|
export const cursorRulesDir = () => join(cwd(), ".cursor", "rules");
|
|
27
|
+
export const cursorHooksDir = () => join(cwd(), ".cursor", "hooks");
|
|
27
28
|
export const cursorSkillrepoMdc = () => join(cwd(), ".cursor", "rules", "skillrepo.mdc");
|
|
29
|
+
export const cursorSkillrepoIndex = () => join(cwd(), ".cursor", "skillrepo-index.json");
|
|
30
|
+
export const cursorHooksJson = () => join(cwd(), ".cursor", "hooks.json");
|
|
28
31
|
|
|
29
32
|
// Windsurf (always global — no project-level config)
|
|
30
33
|
export const windsurfDir = () => join(homedir(), ".codeium", "windsurf");
|
|
@@ -3,8 +3,21 @@
|
|
|
3
3
|
* Calls individual mergers and collects results.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { resolve } from "node:path";
|
|
7
|
+
import { readFileSafe, writeFileSafe, writeExecutable } from "./fs-utils.mjs";
|
|
8
|
+
import { claudeSkillrepoMd, claudeSkillrepoIndex, claudeSkillrepoConfig } from "./paths.mjs";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Validate that a server-supplied path stays within an allowed directory.
|
|
12
|
+
* Prevents path traversal attacks from compromised server responses.
|
|
13
|
+
*/
|
|
14
|
+
function assertPathContained(filePath, allowedDir) {
|
|
15
|
+
const abs = resolve(process.cwd(), filePath);
|
|
16
|
+
const boundary = resolve(process.cwd(), allowedDir);
|
|
17
|
+
if (!abs.startsWith(boundary + "/") && abs !== boundary) {
|
|
18
|
+
throw new Error(`Server returned path outside ${allowedDir}: ${filePath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
8
21
|
import { mergeClaudeMcpConfig } from "./mergers/claude-mcp.mjs";
|
|
9
22
|
import { mergeCursorMcpConfig } from "./mergers/cursor-mcp.mjs";
|
|
10
23
|
import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
|
|
@@ -59,10 +72,37 @@ export function writeAllConfigs({ ides, mcpUrl, apiKey, payload }) {
|
|
|
59
72
|
if (ides.includes("cursor")) {
|
|
60
73
|
results.push(mergeCursorMcpConfig(mcpUrl));
|
|
61
74
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
75
|
+
// Write all rule files (per-skill .mdc + aggregate .mdc)
|
|
76
|
+
if (Array.isArray(payload.cursor?.rules)) {
|
|
77
|
+
for (const rule of payload.cursor.rules) {
|
|
78
|
+
assertPathContained(rule.path, ".cursor");
|
|
79
|
+
const existed = readFileSafe(rule.path) !== null;
|
|
80
|
+
writeFileSafe(rule.path, rule.content);
|
|
81
|
+
results.push({ path: rule.path, action: existed ? "updated" : "created" });
|
|
82
|
+
}
|
|
83
|
+
} else if (payload.cursor?.skillrepoMdc) {
|
|
84
|
+
// Legacy fallback: single .mdc file (pre-v2 server)
|
|
85
|
+
const existed = readFileSafe(payload.cursor.skillrepoMdc.path) !== null;
|
|
86
|
+
writeFileSafe(payload.cursor.skillrepoMdc.path, payload.cursor.skillrepoMdc.content);
|
|
87
|
+
results.push({ path: payload.cursor.skillrepoMdc.path, action: existed ? "updated" : "created" });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Cursor hooks and skill index
|
|
91
|
+
if (payload.cursor?.hooksJson) {
|
|
92
|
+
const existed = readFileSafe(payload.cursor.hooksJson.path) !== null;
|
|
93
|
+
writeFileSafe(payload.cursor.hooksJson.path, payload.cursor.hooksJson.content);
|
|
94
|
+
results.push({ path: payload.cursor.hooksJson.path, action: existed ? "updated" : "created" });
|
|
95
|
+
}
|
|
96
|
+
if (payload.cursor?.sessionHook) {
|
|
97
|
+
const existed = readFileSafe(payload.cursor.sessionHook.path) !== null;
|
|
98
|
+
writeExecutable(payload.cursor.sessionHook.path, payload.cursor.sessionHook.content);
|
|
99
|
+
results.push({ path: payload.cursor.sessionHook.path, action: existed ? "updated" : "created" });
|
|
100
|
+
}
|
|
101
|
+
if (payload.cursor?.skillIndex) {
|
|
102
|
+
const existed = readFileSafe(payload.cursor.skillIndex.path) !== null;
|
|
103
|
+
writeFileSafe(payload.cursor.skillIndex.path, payload.cursor.skillIndex.content);
|
|
104
|
+
results.push({ path: payload.cursor.skillIndex.path, action: existed ? "updated" : "created" });
|
|
105
|
+
}
|
|
66
106
|
}
|
|
67
107
|
|
|
68
108
|
// Windsurf
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# CLI E2E Test Harness — Handoff
|
|
2
|
+
|
|
3
|
+
## What This Is
|
|
4
|
+
|
|
5
|
+
A new agent needs to build a CLI E2E test platform for SkillRepo's `npx skillrepo init` command. The feature code is complete and merged (PR #479). The CLI E2E tests are the missing piece — no test code has been written yet, only the branch and directory exist.
|
|
6
|
+
|
|
7
|
+
## Current State
|
|
8
|
+
|
|
9
|
+
**Branch:** `feat/457-cli-e2e-tests` (off `feat/457-integration`)
|
|
10
|
+
**Directory created:** `packages/cli/src/test/e2e/` (empty)
|
|
11
|
+
**Approved plan:** `/Users/jpace/.claude/plans/valiant-scribbling-whisper.md`
|
|
12
|
+
|
|
13
|
+
## What Was Built (the feature being tested)
|
|
14
|
+
|
|
15
|
+
Epic #457: "Deterministic Skill Injection — Cross-Integration Architecture"
|
|
16
|
+
|
|
17
|
+
The CLI (`npx skillrepo init`) now generates:
|
|
18
|
+
|
|
19
|
+
**Claude Code files:**
|
|
20
|
+
- `.claude/skillrepo.md` — skill reference
|
|
21
|
+
- `.claude/skillrepo-index.json` — skill index with `contextSignals` (files, project, tasks)
|
|
22
|
+
- `.claude/skillrepo-config.json` — version 2 config with `signals` array and `companions` map
|
|
23
|
+
- `.claude/hooks/skillrepo-sync.mjs` — SessionStart hook (refreshes configs + repo profiling)
|
|
24
|
+
- `.claude/hooks/skillrepo-prompt-match.mjs` — UserPromptSubmit hook (weighted scoring + profile multiplier)
|
|
25
|
+
- `.claude/hooks/skillrepo-pretool-match.mjs` — PreToolUse hook (dynamic signal map + glob matching)
|
|
26
|
+
- `.claude/settings.local.json` — hook registration (merged with existing)
|
|
27
|
+
- `.mcp.json` — MCP server config
|
|
28
|
+
|
|
29
|
+
**Cursor files:**
|
|
30
|
+
- `.cursor/rules/skillrepo-{name}.mdc` — per-skill rules with `globs` for file-based activation
|
|
31
|
+
- `.cursor/rules/skillrepo.mdc` — aggregate `alwaysApply` rule for skills without file signals
|
|
32
|
+
- `.cursor/hooks.json` — registers `sessionStart` hook
|
|
33
|
+
- `.cursor/hooks/skillrepo-session.mjs` — session hook with repo profiling + project-aware context
|
|
34
|
+
- `.cursor/skillrepo-index.json` — skill index (same as Claude Code)
|
|
35
|
+
- `.cursor/mcp.json` — MCP server config
|
|
36
|
+
|
|
37
|
+
**Both:**
|
|
38
|
+
- `.env.local` — `SKILLREPO_ACCESS_KEY=...`
|
|
39
|
+
|
|
40
|
+
## Key Code Paths
|
|
41
|
+
|
|
42
|
+
| File | What it does |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `packages/cli/bin/skillrepo.mjs` | CLI entry point |
|
|
45
|
+
| `packages/cli/src/commands/init.mjs` | Init command: detect IDEs → prompt for key → fetch payload → write configs |
|
|
46
|
+
| `packages/cli/src/lib/http.mjs` | `fetchSetupPayload(apiKey, baseUrl)` — GET `/api/v1/setup` with Bearer auth |
|
|
47
|
+
| `packages/cli/src/lib/write-configs.mjs` | Orchestrates all file writes per IDE, includes path traversal protection |
|
|
48
|
+
| `packages/cli/src/lib/paths.mjs` | All output file path constants |
|
|
49
|
+
| `packages/cli/src/lib/detect-ides.mjs` | Scans for `.claude/`, `.cursor/`, `.vscode/` directories |
|
|
50
|
+
| `packages/cli/src/lib/fs-utils.mjs` | `readFileSafe()`, `writeFileSafe()` |
|
|
51
|
+
| `packages/cli/src/lib/mergers/` | Individual mergers for settings.local.json, mcp.json, etc. |
|
|
52
|
+
| `src/lib/setup/generate.ts` | Server-side: `generateSetupPayload()` produces the full payload |
|
|
53
|
+
|
|
54
|
+
## The HTTP Contract
|
|
55
|
+
|
|
56
|
+
The CLI makes ONE HTTP call:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
GET {baseUrl}/api/v1/setup
|
|
60
|
+
Authorization: Bearer sk_live_...
|
|
61
|
+
Accept: application/json
|
|
62
|
+
User-Agent: skillrepo-cli/1.0.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Response is a `SetupPayload` JSON object (see `src/lib/setup/generate.ts` for the type).
|
|
66
|
+
|
|
67
|
+
## What To Build
|
|
68
|
+
|
|
69
|
+
### Approach: Subprocess + Mock HTTP Server
|
|
70
|
+
|
|
71
|
+
Tests run the actual CLI binary (`node bin/skillrepo.mjs init`) as a child process against a lightweight mock HTTP server. This tests what users actually experience.
|
|
72
|
+
|
|
73
|
+
### Test Runner
|
|
74
|
+
|
|
75
|
+
Use `node:test` (Node.js built-in) — consistent with existing CLI tests in `packages/cli/src/test/`. Coverage via `c8` wrapping the subprocess.
|
|
76
|
+
|
|
77
|
+
### Files To Create
|
|
78
|
+
|
|
79
|
+
| File | Purpose |
|
|
80
|
+
|------|---------|
|
|
81
|
+
| `packages/cli/src/test/e2e/mock-server.mjs` | Minimal `node:http` server returning configurable `SetupPayload` |
|
|
82
|
+
| `packages/cli/src/test/e2e/payload-factory.mjs` | Builds realistic payloads with various skill configurations |
|
|
83
|
+
| `packages/cli/src/test/e2e/cli-init.test.mjs` | Full init flow E2E tests |
|
|
84
|
+
|
|
85
|
+
### Mock Server Requirements
|
|
86
|
+
|
|
87
|
+
- Listens on random available port
|
|
88
|
+
- Serves `GET /api/v1/setup` with configurable payload
|
|
89
|
+
- Validates `Authorization: Bearer sk_live_...` header
|
|
90
|
+
- Returns 401 for invalid/missing keys
|
|
91
|
+
- Returns 200 with payload for valid keys
|
|
92
|
+
- Shuts down cleanly after tests
|
|
93
|
+
|
|
94
|
+
### Payload Factory Requirements
|
|
95
|
+
|
|
96
|
+
Must generate realistic `SetupPayload` objects matching what `generateSetupPayload()` produces. Include:
|
|
97
|
+
|
|
98
|
+
1. Skills WITH contextSignals (files, project, tasks populated)
|
|
99
|
+
2. Skills WITHOUT contextSignals (null)
|
|
100
|
+
3. Skills with special characters in descriptions (colons, quotes — YAML safety)
|
|
101
|
+
4. Configurable skill count
|
|
102
|
+
5. Both Claude Code and Cursor output structures
|
|
103
|
+
|
|
104
|
+
Reference `src/lib/setup/generate.ts` for the exact `SetupPayload` type:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
interface SetupPayload {
|
|
108
|
+
skillCount: number;
|
|
109
|
+
mcpUrl: string;
|
|
110
|
+
skillEntries: string[];
|
|
111
|
+
claudeCode: {
|
|
112
|
+
skillrepoMd: { path: string; content: string };
|
|
113
|
+
syncHook: { path: string; content: string };
|
|
114
|
+
settingsHooks: { hooks: Record<string, unknown> };
|
|
115
|
+
skillIndex: { path: string; content: string };
|
|
116
|
+
skillrepoConfig: { path: string; content: string };
|
|
117
|
+
promptHook: { path: string; content: string };
|
|
118
|
+
preToolHook: { path: string; content: string };
|
|
119
|
+
};
|
|
120
|
+
cursor: {
|
|
121
|
+
rules: Array<{ path: string; content: string }>;
|
|
122
|
+
hooksJson: { path: string; content: string };
|
|
123
|
+
sessionHook: { path: string; content: string };
|
|
124
|
+
skillIndex: { path: string; content: string };
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Test Cases (goal-driven, not implementation-driven)
|
|
130
|
+
|
|
131
|
+
Tests verify OUTCOMES ("does this produce a working setup?") not IMPLEMENTATION ("does the string contain this function name?").
|
|
132
|
+
|
|
133
|
+
**1. Full Init Flow**
|
|
134
|
+
- `init --yes --url http://localhost:{port} --key sk_live_test` in a temp dir produces all expected files
|
|
135
|
+
- Running init twice produces correct merge behavior (updated, not duplicated)
|
|
136
|
+
- Invalid API key returns clean auth error message
|
|
137
|
+
- Unreachable server returns clean network error message
|
|
138
|
+
|
|
139
|
+
**2. Generated Files Are Structurally Valid**
|
|
140
|
+
- All JSON files (`skillrepo-config.json`, `skillrepo-index.json`, `hooks.json`, `settings.local.json`, `mcp.json`) parse without error
|
|
141
|
+
- YAML frontmatter in `.mdc` files parses correctly (including descriptions with colons)
|
|
142
|
+
- Hook scripts compile as valid JavaScript (`vm.compileFunction` or `new Function()`)
|
|
143
|
+
|
|
144
|
+
**3. Signal Map Works End-to-End**
|
|
145
|
+
- Extract `matchGlob` from the generated PreToolUse hook script, eval it, and test against real file paths:
|
|
146
|
+
- `**/*.tsx` matches `src/components/Button.tsx` ✓
|
|
147
|
+
- `**/*.tsx` does NOT match `src/utils/helper.ts` ✗
|
|
148
|
+
- `**/*.test.*` matches `tests/foo.test.ts` ✓
|
|
149
|
+
- `**/api/**/route.ts` matches `src/app/api/users/route.ts` ✓
|
|
150
|
+
- `**/*.{ts,tsx}` does not crash (regex metachar escaping) ✓
|
|
151
|
+
- Extract `extractFilePath` and verify it parses tool input correctly
|
|
152
|
+
- Extract `scoreSkills` and verify: task signals = 3x weight, keywords = 1x
|
|
153
|
+
- Extract `applyProfileMultiplier` and verify: matching project = 1.5x, no match = 0.3x, no tags = 1.0x
|
|
154
|
+
|
|
155
|
+
**4. Cursor Output Goals**
|
|
156
|
+
- Skills with `contextSignals.files` produce per-skill `.mdc` with `globs` frontmatter
|
|
157
|
+
- Skills without file signals go to aggregate `skillrepo.mdc` with `alwaysApply: true`
|
|
158
|
+
- No skill appears in both per-skill AND aggregate files
|
|
159
|
+
- `hooks.json` is valid JSON with `sessionStart` event
|
|
160
|
+
- Session hook script produces valid `additional_context` JSON when executed
|
|
161
|
+
|
|
162
|
+
**5. Claude Code Output Goals**
|
|
163
|
+
- Config has `version: 2` and parseable `signals` array
|
|
164
|
+
- Signal entries have toolName, files, project, tasks fields
|
|
165
|
+
- Companions are correctly resolved
|
|
166
|
+
- All three hooks registered in `settings.local.json`
|
|
167
|
+
- Sync hook contains repo profiling logic
|
|
168
|
+
|
|
169
|
+
**6. Repo Profiling**
|
|
170
|
+
- In a simulated Next.js project (temp dir with `package.json` containing `next`, `react`), the sync hook's profiling code detects the correct frameworks
|
|
171
|
+
- Execute the profiling section of the generated sync hook and verify the output
|
|
172
|
+
|
|
173
|
+
**7. Security**
|
|
174
|
+
- Server-supplied `.mdc` path outside `.cursor/` is rejected with error (path traversal protection)
|
|
175
|
+
|
|
176
|
+
### How To Extract and Test Generated Functions
|
|
177
|
+
|
|
178
|
+
The generated hook scripts are JavaScript strings. To test functions like `matchGlob`:
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
import { readFileSync } from 'fs';
|
|
182
|
+
|
|
183
|
+
// Read the generated hook script
|
|
184
|
+
const script = readFileSync('.claude/hooks/skillrepo-pretool-match.mjs', 'utf-8');
|
|
185
|
+
|
|
186
|
+
// Extract matchGlob function
|
|
187
|
+
const matchGlobMatch = script.match(/function matchGlob\(filePath, pattern\) \{[\s\S]*?\n\}/);
|
|
188
|
+
const matchGlobBody = matchGlobMatch[0]
|
|
189
|
+
.replace(/^function matchGlob\(filePath, pattern\) \{/, '')
|
|
190
|
+
.replace(/\}$/, '');
|
|
191
|
+
const matchGlob = new Function('filePath', 'pattern', matchGlobBody);
|
|
192
|
+
|
|
193
|
+
// Now test it
|
|
194
|
+
assert.strictEqual(matchGlob('src/components/Button.tsx', '**/*.tsx'), true);
|
|
195
|
+
assert.strictEqual(matchGlob('src/utils/helper.ts', '**/*.tsx'), false);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Existing Test Patterns to Follow
|
|
199
|
+
|
|
200
|
+
Look at `packages/cli/src/test/` for the established patterns:
|
|
201
|
+
- `detect-ides.test.mjs` — temp directory creation, `process.cwd()` override
|
|
202
|
+
- `mergers/hooks-json.test.mjs` — file merge testing (19 test cases)
|
|
203
|
+
- `env-local.test.mjs` — `.env.local` merge behavior
|
|
204
|
+
|
|
205
|
+
All use `node:test`, `node:assert`, `mkdtempSync`, `rmSync` for cleanup.
|
|
206
|
+
|
|
207
|
+
### Coverage
|
|
208
|
+
|
|
209
|
+
Use `c8` to instrument the CLI subprocess. Add to root `package.json`:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
"test:cli-e2e": "cd packages/cli && c8 node --test src/test/e2e/*.test.mjs"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Coverage output should be merged into the overall codecov upload in `.github/workflows/e2e.yml`.
|
|
216
|
+
|
|
217
|
+
### Running the Tests
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# From repo root
|
|
221
|
+
cd packages/cli && node --test src/test/e2e/cli-init.test.mjs
|
|
222
|
+
|
|
223
|
+
# With coverage
|
|
224
|
+
cd packages/cli && c8 node --test src/test/e2e/cli-init.test.mjs
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Important Rules (from CLAUDE.md and memory)
|
|
228
|
+
|
|
229
|
+
- NEVER edit `.claude/` generated files directly — modify generators in `src/lib/setup/generate.ts`
|
|
230
|
+
- NEVER run tests against production — require explicit `DATABASE_URL_E2E` (this harness doesn't need a DB since it mocks the HTTP server)
|
|
231
|
+
- Always use GitHub Issues, never Asana
|
|
232
|
+
- Run `npm run check` before considering work complete
|
|
233
|
+
- CLI is at `packages/cli/` — it's a standalone package with its own `package.json`
|
|
234
|
+
- CLI version is currently 1.6.0 — bump if CLI code changes
|
|
235
|
+
|
|
236
|
+
## References
|
|
237
|
+
|
|
238
|
+
- Epic: https://github.com/atxpace/skill-repo/issues/457
|
|
239
|
+
- Integration PR (merged): https://github.com/atxpace/skill-repo/pull/479
|
|
240
|
+
- Review fixes PR: https://github.com/atxpace/skill-repo/pull/480
|
|
241
|
+
- Approved plan: `/Users/jpace/.claude/plans/valiant-scribbling-whisper.md`
|
|
242
|
+
- Existing CLI tests: `packages/cli/src/test/`
|
|
243
|
+
- Server-side generator: `src/lib/setup/generate.ts`
|
|
244
|
+
- CLI write orchestrator: `packages/cli/src/lib/write-configs.mjs`
|