skillrepo 2.0.0 → 3.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.
Files changed (72) hide show
  1. package/README.md +276 -145
  2. package/bin/skillrepo.mjs +224 -36
  3. package/package.json +6 -3
  4. package/src/commands/add.mjs +176 -0
  5. package/src/commands/get.mjs +116 -0
  6. package/src/commands/init.mjs +589 -143
  7. package/src/commands/list.mjs +176 -0
  8. package/src/commands/remove.mjs +162 -0
  9. package/src/commands/search.mjs +188 -0
  10. package/src/commands/session-sync.mjs +152 -0
  11. package/src/commands/uninstall.mjs +484 -0
  12. package/src/commands/update.mjs +184 -0
  13. package/src/lib/artifact-registry.mjs +265 -0
  14. package/src/lib/cli-config.mjs +230 -0
  15. package/src/lib/config.mjs +238 -0
  16. package/src/lib/detect-ides.mjs +0 -19
  17. package/src/lib/errors.mjs +264 -0
  18. package/src/lib/file-write.mjs +705 -0
  19. package/src/lib/fs-utils.mjs +83 -1
  20. package/src/lib/http.mjs +817 -37
  21. package/src/lib/identifier.mjs +153 -0
  22. package/src/lib/mcp-merge.mjs +275 -0
  23. package/src/lib/mergers/gitignore.mjs +73 -18
  24. package/src/lib/mergers/session-hook.mjs +298 -0
  25. package/src/lib/paths.mjs +67 -17
  26. package/src/lib/prompt.mjs +11 -44
  27. package/src/lib/removers/claude-mcp.mjs +67 -0
  28. package/src/lib/removers/cursor-mcp.mjs +60 -0
  29. package/src/lib/removers/env-local.mjs +55 -0
  30. package/src/lib/removers/gitignore.mjs +108 -0
  31. package/src/lib/removers/settings.mjs +183 -0
  32. package/src/lib/removers/vscode-mcp.mjs +87 -0
  33. package/src/lib/removers/windsurf-mcp.mjs +65 -0
  34. package/src/lib/sync.mjs +305 -0
  35. package/src/test/commands/add.test.mjs +285 -0
  36. package/src/test/commands/get.test.mjs +176 -0
  37. package/src/test/commands/init.test.mjs +697 -0
  38. package/src/test/commands/list.test.mjs +172 -0
  39. package/src/test/commands/remove.test.mjs +234 -0
  40. package/src/test/commands/search.test.mjs +204 -0
  41. package/src/test/commands/session-sync.test.mjs +350 -0
  42. package/src/test/commands/uninstall.test.mjs +768 -0
  43. package/src/test/commands/update.test.mjs +322 -0
  44. package/src/test/detect-ides.test.mjs +9 -14
  45. package/src/test/dispatcher.test.mjs +224 -0
  46. package/src/test/e2e/cli-commands.test.mjs +576 -0
  47. package/src/test/e2e/mock-server.mjs +364 -22
  48. package/src/test/helpers/capture-stream.mjs +48 -0
  49. package/src/test/integration/file-write.integration.test.mjs +279 -0
  50. package/src/test/lib/artifact-registry.test.mjs +268 -0
  51. package/src/test/lib/cli-config.test.mjs +407 -0
  52. package/src/test/lib/config.test.mjs +257 -0
  53. package/src/test/lib/errors.test.mjs +359 -0
  54. package/src/test/lib/file-write.test.mjs +784 -0
  55. package/src/test/lib/http.test.mjs +1198 -0
  56. package/src/test/lib/identifier.test.mjs +157 -0
  57. package/src/test/lib/mcp-merge.test.mjs +345 -0
  58. package/src/test/lib/paths.test.mjs +83 -0
  59. package/src/test/lib/sync.test.mjs +514 -0
  60. package/src/test/mergers/gitignore.test.mjs +145 -20
  61. package/src/test/mergers/session-hook.test.mjs +745 -0
  62. package/src/test/mergers/uninstall-claude-mcp.test.mjs +145 -0
  63. package/src/test/mergers/uninstall-cursor-mcp.test.mjs +108 -0
  64. package/src/test/mergers/uninstall-env-local.test.mjs +144 -0
  65. package/src/test/mergers/uninstall-gitignore.test.mjs +209 -0
  66. package/src/test/mergers/uninstall-settings.test.mjs +285 -0
  67. package/src/test/mergers/uninstall-vscode-mcp.test.mjs +215 -0
  68. package/src/test/mergers/uninstall-windsurf-mcp.test.mjs +122 -0
  69. package/src/lib/write-configs.mjs +0 -202
  70. package/src/test/e2e/HANDOFF.md +0 -223
  71. package/src/test/e2e/cli-init.test.mjs +0 -213
  72. package/src/test/e2e/payload-factory.mjs +0 -22
@@ -1,202 +0,0 @@
1
- /**
2
- * Orchestrates writing all config files based on detected IDEs.
3
- * Calls individual mergers and collects results.
4
- *
5
- * Phase C (#535): Simplified to install standalone hooks and global config.
6
- * Skill mapping files (.claude/skillrepo.md, .claude/skillrepo-index.json,
7
- * .claude/skillrepo-config.json) are no longer written — the sync hook
8
- * handles all skill delivery via .claude/rules/ files.
9
- */
10
-
11
- import { resolve, join } from "node:path";
12
- import { existsSync, mkdirSync, readdirSync, unlinkSync } from "node:fs";
13
- import { readFileSafe, writeFileSafe } from "./fs-utils.mjs";
14
- import { globalSkillrepoDir, globalConfigPath, claudeRulesDir } from "./paths.mjs";
15
-
16
- import { mergeClaudeMcpConfig } from "./mergers/claude-mcp.mjs";
17
- import { mergeCursorMcpConfig } from "./mergers/cursor-mcp.mjs";
18
- import { mergeWindsurfMcpConfig } from "./mergers/windsurf-mcp.mjs";
19
- import { mergeVscodeMcpConfig } from "./mergers/vscode-mcp.mjs";
20
- import { mergeEnvLocal } from "./mergers/env-local.mjs";
21
- import { mergeGitignore } from "./mergers/gitignore.mjs";
22
-
23
- /**
24
- * Write all configuration files.
25
- * @param {object} options
26
- * @param {string[]} options.ides - IDE keys to configure (claudeCode, cursor, windsurf, vscode)
27
- * @param {string} options.mcpUrl - The MCP endpoint URL
28
- * @param {string} options.apiKey - The access key
29
- * @param {string} options.serverUrl - The SkillRepo server URL (e.g. https://skillrepo.dev)
30
- * @param {string} [options.userId] - The authenticated user's SkillRepo ID
31
- * @returns {{ path: string; action: string }[]}
32
- */
33
- export function writeAllConfigs({ ides, mcpUrl, apiKey, serverUrl, userId }) {
34
- const results = [];
35
-
36
- // ── Global config (shared across all projects) ────────────────────────
37
- const globalConfigAction = writeGlobalConfig(apiKey, serverUrl, userId);
38
- results.push({ path: "~/.claude/skillrepo/config.json", action: globalConfigAction });
39
-
40
- // Claude Code
41
- if (ides.includes("claudeCode")) {
42
- results.push(mergeClaudeMcpConfig(mcpUrl));
43
-
44
- // Create .claude/rules/ directory for skill delivery
45
- const rulesDir = claudeRulesDir();
46
- if (!existsSync(rulesDir)) {
47
- mkdirSync(rulesDir, { recursive: true });
48
- results.push({ path: ".claude/rules/", action: "created" });
49
- } else {
50
- results.push({ path: ".claude/rules/", action: "skipped" });
51
- }
52
-
53
- // Add .gitignore entry for skillrepo rules files
54
- results.push(mergeGitignore());
55
-
56
- // Clean up old-format files from pre-Phase C installs.
57
- // Write a migration marker BEFORE cleanup so the sync hook can detect
58
- // the upgrade and show a one-time "restart session" message.
59
- writeMigrationMarker();
60
- cleanupLegacyFiles(results);
61
- }
62
-
63
- // Cursor
64
- if (ides.includes("cursor")) {
65
- results.push(mergeCursorMcpConfig(mcpUrl));
66
-
67
- // Clean up old-format ownerless Cursor rules files
68
- cleanupLegacyCursorRules(results);
69
- }
70
-
71
- // Windsurf
72
- if (ides.includes("windsurf")) {
73
- results.push(mergeWindsurfMcpConfig(mcpUrl));
74
- }
75
-
76
- // VS Code + Copilot
77
- if (ides.includes("vscode")) {
78
- results.push(mergeVscodeMcpConfig(mcpUrl));
79
- }
80
-
81
- // .env.local (always — backward compat for hooks' config fallback)
82
- results.push(mergeEnvLocal(apiKey));
83
-
84
- return results;
85
- }
86
-
87
- /**
88
- * Write the global SkillRepo config file.
89
- * This is the primary config source for standalone hooks.
90
- * @returns {"created" | "updated"} The action taken.
91
- */
92
- function writeGlobalConfig(apiKey, serverUrl, userId) {
93
- const configDir = globalSkillrepoDir();
94
- if (!existsSync(configDir)) {
95
- mkdirSync(configDir, { recursive: true });
96
- }
97
-
98
- const configPath = globalConfigPath();
99
- const existingRaw = readFileSafe(configPath);
100
-
101
- // Preserve existing config fields (e.g. maxRulesFiles, maxRulesBudgetBytes)
102
- // while always overwriting apiKey, serverUrl, and userId with the new values.
103
- let preserved = {};
104
- if (existingRaw !== null) {
105
- try { preserved = JSON.parse(existingRaw); }
106
- catch { /* corrupt config — start fresh */ }
107
- }
108
- // Always overwrite userId — even if the new value is undefined/null — to
109
- // prevent a stale userId from a previous account persisting across re-inits.
110
- const { userId: _prevUserId, ...preservedWithoutUserId } = preserved;
111
- const config = { ...preservedWithoutUserId, apiKey, serverUrl };
112
- if (userId) config.userId = userId;
113
-
114
- writeFileSafe(configPath, JSON.stringify(config, null, 2) + "\n");
115
- return existingRaw !== null ? "updated" : "created";
116
- }
117
-
118
- /**
119
- * Write a migration marker if old-format files exist.
120
- * The sync hook checks this marker to show a one-time "restart session"
121
- * message, then deletes it. This is needed because init cleans up old
122
- * files before the sync hook runs, so the hook can't detect the upgrade
123
- * by looking for old files directly.
124
- */
125
- function writeMigrationMarker() {
126
- const markerPath = join(process.cwd(), ".claude", "skillrepo-migrated");
127
- const oldFiles = [
128
- join(process.cwd(), ".claude", "skillrepo-config.json"),
129
- join(process.cwd(), ".claude", "skillrepo.md"),
130
- ];
131
-
132
- const hasOldFiles = oldFiles.some(f => existsSync(f));
133
- if (hasOldFiles) {
134
- writeFileSafe(markerPath, new Date().toISOString() + "\n");
135
- }
136
- }
137
-
138
- /**
139
- * Remove old-format files from pre-Phase C installs.
140
- * These files are replaced by the sync hook's rules-based delivery.
141
- */
142
- function cleanupLegacyFiles(results) {
143
- const legacyFiles = [
144
- { path: ".claude/skillrepo.md", abs: join(process.cwd(), ".claude", "skillrepo.md") },
145
- { path: ".claude/skillrepo-index.json", abs: join(process.cwd(), ".claude", "skillrepo-index.json") },
146
- { path: ".claude/skillrepo-config.json", abs: join(process.cwd(), ".claude", "skillrepo-config.json") },
147
- ];
148
-
149
- for (const file of legacyFiles) {
150
- try {
151
- unlinkSync(file.abs);
152
- results.push({ path: file.path, action: "removed" });
153
- } catch {
154
- // File doesn't exist — nothing to clean up
155
- }
156
- }
157
- }
158
-
159
- /**
160
- * Remove old-format ownerless Cursor rules files.
161
- * Pre-Phase C used `.cursor/rules/skillrepo-{name}.mdc` without owner prefix.
162
- * Phase B+ uses `.cursor/rules/skillrepo-{owner}-{name}.mdc`.
163
- *
164
- * Heuristic: a file is old-format if its name after "skillrepo-" contains
165
- * no hyphen (single segment = name only) or matches known ownerless patterns.
166
- * Since owner slugs always contain at least one character and names always
167
- * contain at least one character, the new format always has at least two
168
- * segments after "skillrepo-" separated by a hyphen.
169
- *
170
- * We detect ownerless files by checking: after stripping "skillrepo-" prefix
171
- * and ".mdc" suffix, if the remainder has no hyphen, it's definitely ownerless.
172
- * If it has hyphens, we can't distinguish (could be owner-name or multi-word name).
173
- * For safety, we only clean up the definitely-ownerless ones.
174
- */
175
- function cleanupLegacyCursorRules(results) {
176
- const cursorRulesDir = join(process.cwd(), ".cursor", "rules");
177
- let entries;
178
- try { entries = readdirSync(cursorRulesDir); }
179
- catch { return; }
180
-
181
- for (const entry of entries) {
182
- if (!entry.startsWith("skillrepo-") || !entry.endsWith(".mdc")) continue;
183
-
184
- // The aggregate file (skillrepo.mdc) is also legacy — handled by sync hook
185
- const baseName = entry.slice("skillrepo-".length, -".mdc".length);
186
-
187
- // Old format: skillrepo-{name}.mdc (no owner prefix, no hyphen in baseName)
188
- // New format: skillrepo-{owner}-{name}.mdc (at least one hyphen)
189
- if (!baseName.includes("-")) {
190
- try {
191
- unlinkSync(join(cursorRulesDir, entry));
192
- results.push({ path: `.cursor/rules/${entry}`, action: "removed" });
193
- } catch { /* ignore */ }
194
- }
195
- }
196
-
197
- // Also remove the aggregate skillrepo.mdc (no longer used)
198
- try {
199
- unlinkSync(join(cursorRulesDir, "skillrepo.mdc"));
200
- results.push({ path: ".cursor/rules/skillrepo.mdc", action: "removed" });
201
- } catch { /* doesn't exist */ }
202
- }
@@ -1,223 +0,0 @@
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-pretool-match.mjs` — PreToolUse hook (dynamic signal map + glob matching)
24
- - `.claude/settings.local.json` — hook registration (merged with existing)
25
- - `.mcp.json` — MCP server config
26
-
27
- **Cursor files:**
28
- - `.cursor/rules/skillrepo-{name}.mdc` — per-skill rules with `globs` for file-based activation
29
- - `.cursor/rules/skillrepo.mdc` — aggregate `alwaysApply` rule for skills without file signals
30
- - `.cursor/hooks.json` — registers `sessionStart` hook
31
- - `.cursor/hooks/skillrepo-session.mjs` — session hook with repo profiling + project-aware context
32
- - `.cursor/skillrepo-index.json` — skill index (same as Claude Code)
33
- - `.cursor/mcp.json` — MCP server config
34
-
35
- **Both:**
36
- - `.env.local` — `SKILLREPO_ACCESS_KEY=...`
37
-
38
- ## Key Code Paths
39
-
40
- | File | What it does |
41
- |------|-------------|
42
- | `packages/cli/bin/skillrepo.mjs` | CLI entry point |
43
- | `packages/cli/src/commands/init.mjs` | Init command: detect IDEs → prompt for key → fetch payload → write configs |
44
- | `packages/cli/src/lib/http.mjs` | `fetchSetupPayload(apiKey, baseUrl)` — GET `/api/v1/setup` with Bearer auth |
45
- | `packages/cli/src/lib/write-configs.mjs` | Orchestrates all file writes per IDE, includes path traversal protection |
46
- | `packages/cli/src/lib/paths.mjs` | All output file path constants |
47
- | `packages/cli/src/lib/detect-ides.mjs` | Scans for `.claude/`, `.cursor/`, `.vscode/` directories |
48
- | `packages/cli/src/lib/fs-utils.mjs` | `readFileSafe()`, `writeFileSafe()` |
49
- | `packages/cli/src/lib/mergers/` | Individual mergers for settings.local.json, mcp.json, etc. |
50
- | `src/lib/setup/generate.ts` | Server-side: `generateSetupPayload()` produces the full payload |
51
-
52
- ## The HTTP Contract
53
-
54
- The CLI makes ONE HTTP call:
55
-
56
- ```
57
- GET {baseUrl}/api/v1/setup
58
- Authorization: Bearer sk_live_...
59
- Accept: application/json
60
- User-Agent: skillrepo-cli/1.0.0
61
- ```
62
-
63
- Response is a `SetupPayload` JSON object (see `src/lib/setup/generate.ts` for the type).
64
-
65
- ## What To Build
66
-
67
- ### Approach: Subprocess + Mock HTTP Server
68
-
69
- 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.
70
-
71
- ### Test Runner
72
-
73
- Use `node:test` (Node.js built-in) — consistent with existing CLI tests in `packages/cli/src/test/`. Coverage via `c8` wrapping the subprocess.
74
-
75
- ### Files To Create
76
-
77
- | File | Purpose |
78
- |------|---------|
79
- | `packages/cli/src/test/e2e/mock-server.mjs` | Minimal `node:http` server returning configurable `SetupPayload` |
80
- | `packages/cli/src/test/e2e/payload-factory.mjs` | Builds realistic payloads with various skill configurations |
81
- | `packages/cli/src/test/e2e/cli-init.test.mjs` | Full init flow E2E tests |
82
-
83
- ### Mock Server Requirements
84
-
85
- - Listens on random available port
86
- - Serves `GET /api/v1/setup` with configurable payload
87
- - Validates `Authorization: Bearer sk_live_...` header
88
- - Returns 401 for invalid/missing keys
89
- - Returns 200 with payload for valid keys
90
- - Shuts down cleanly after tests
91
-
92
- ### Payload Factory Requirements
93
-
94
- Must generate `SetupPayload` objects matching what `generateSetupPayload()` produces.
95
-
96
- Phase D (#534) stripped the payload to a minimal shape — the CLI only uses `skillCount`.
97
- All other setup work (hooks, rules, sync) is handled by standalone scripts bundled
98
- with the CLI package.
99
-
100
- Reference `src/lib/setup/generate.ts` for the exact `SetupPayload` type:
101
-
102
- ```typescript
103
- interface SetupPayload {
104
- skillCount: number;
105
- mcpUrl: string;
106
- }
107
- ```
108
-
109
- ### Test Cases (goal-driven, not implementation-driven)
110
-
111
- Tests verify OUTCOMES ("does this produce a working setup?") not IMPLEMENTATION ("does the string contain this function name?").
112
-
113
- **1. Full Init Flow**
114
- - `init --yes --url http://localhost:{port} --key sk_live_test` in a temp dir produces all expected files
115
- - Running init twice produces correct merge behavior (updated, not duplicated)
116
- - Invalid API key returns clean auth error message
117
- - Unreachable server returns clean network error message
118
-
119
- **2. Generated Files Are Structurally Valid**
120
- - All JSON files (`skillrepo-config.json`, `skillrepo-index.json`, `hooks.json`, `settings.local.json`, `mcp.json`) parse without error
121
- - YAML frontmatter in `.mdc` files parses correctly (including descriptions with colons)
122
- - Hook scripts compile as valid JavaScript (`vm.compileFunction` or `new Function()`)
123
-
124
- **3. Signal Map Works End-to-End**
125
- - Extract `matchGlob` from the generated PreToolUse hook script, eval it, and test against real file paths:
126
- - `**/*.tsx` matches `src/components/Button.tsx` ✓
127
- - `**/*.tsx` does NOT match `src/utils/helper.ts` ✗
128
- - `**/*.test.*` matches `tests/foo.test.ts` ✓
129
- - `**/api/**/route.ts` matches `src/app/api/users/route.ts` ✓
130
- - `**/*.{ts,tsx}` does not crash (regex metachar escaping) ✓
131
- - Extract `extractFilePath` and verify it parses tool input correctly
132
- - Extract `scoreSkills` and verify: task signals = 3x weight, keywords = 1x
133
- - Extract `applyProfileMultiplier` and verify: matching project = 1.5x, no match = 0.3x, no tags = 1.0x
134
-
135
- **4. Cursor Output Goals**
136
- - Skills with `contextSignals.files` produce per-skill `.mdc` with `globs` frontmatter
137
- - Skills without file signals go to aggregate `skillrepo.mdc` with `alwaysApply: true`
138
- - No skill appears in both per-skill AND aggregate files
139
- - `hooks.json` is valid JSON with `sessionStart` event
140
- - Session hook script produces valid `additional_context` JSON when executed
141
-
142
- **5. Claude Code Output Goals**
143
- - Config has `version: 2` and parseable `signals` array
144
- - Signal entries have toolName, files, project, tasks fields
145
- - Companions are correctly resolved
146
- - All three hooks registered in `settings.local.json`
147
- - Sync hook contains repo profiling logic
148
-
149
- **6. Repo Profiling**
150
- - In a simulated Next.js project (temp dir with `package.json` containing `next`, `react`), the sync hook's profiling code detects the correct frameworks
151
- - Execute the profiling section of the generated sync hook and verify the output
152
-
153
- **7. Security**
154
- - Server-supplied `.mdc` path outside `.cursor/` is rejected with error (path traversal protection)
155
-
156
- ### How To Extract and Test Generated Functions
157
-
158
- The generated hook scripts are JavaScript strings. To test functions like `matchGlob`:
159
-
160
- ```javascript
161
- import { readFileSync } from 'fs';
162
-
163
- // Read the generated hook script
164
- const script = readFileSync('.claude/hooks/skillrepo-pretool-match.mjs', 'utf-8');
165
-
166
- // Extract matchGlob function
167
- const matchGlobMatch = script.match(/function matchGlob\(filePath, pattern\) \{[\s\S]*?\n\}/);
168
- const matchGlobBody = matchGlobMatch[0]
169
- .replace(/^function matchGlob\(filePath, pattern\) \{/, '')
170
- .replace(/\}$/, '');
171
- const matchGlob = new Function('filePath', 'pattern', matchGlobBody);
172
-
173
- // Now test it
174
- assert.strictEqual(matchGlob('src/components/Button.tsx', '**/*.tsx'), true);
175
- assert.strictEqual(matchGlob('src/utils/helper.ts', '**/*.tsx'), false);
176
- ```
177
-
178
- ### Existing Test Patterns to Follow
179
-
180
- Look at `packages/cli/src/test/` for the established patterns:
181
- - `detect-ides.test.mjs` — temp directory creation, `process.cwd()` override
182
- - `env-local.test.mjs` — `.env.local` merge behavior
183
-
184
- All use `node:test`, `node:assert`, `mkdtempSync`, `rmSync` for cleanup.
185
-
186
- ### Coverage
187
-
188
- Use `c8` to instrument the CLI subprocess. Add to root `package.json`:
189
-
190
- ```json
191
- "test:cli-e2e": "cd packages/cli && c8 node --test src/test/e2e/*.test.mjs"
192
- ```
193
-
194
- Coverage output should be merged into the overall codecov upload in `.github/workflows/e2e.yml`.
195
-
196
- ### Running the Tests
197
-
198
- ```bash
199
- # From repo root
200
- cd packages/cli && node --test src/test/e2e/cli-init.test.mjs
201
-
202
- # With coverage
203
- cd packages/cli && c8 node --test src/test/e2e/cli-init.test.mjs
204
- ```
205
-
206
- ## Important Rules (from CLAUDE.md and memory)
207
-
208
- - NEVER edit `.claude/` generated files directly — modify generators in `src/lib/setup/generate.ts`
209
- - NEVER run tests against production — require explicit `DATABASE_URL_E2E` (this harness doesn't need a DB since it mocks the HTTP server)
210
- - Always use GitHub Issues, never Asana
211
- - Run `npm run check` before considering work complete
212
- - CLI is at `packages/cli/` — it's a standalone package with its own `package.json`
213
- - CLI version is currently 1.6.0 — bump if CLI code changes
214
-
215
- ## References
216
-
217
- - Epic: https://github.com/atxpace/skill-repo/issues/457
218
- - Integration PR (merged): https://github.com/atxpace/skill-repo/pull/479
219
- - Review fixes PR: https://github.com/atxpace/skill-repo/pull/480
220
- - Approved plan: `/Users/jpace/.claude/plans/valiant-scribbling-whisper.md`
221
- - Existing CLI tests: `packages/cli/src/test/`
222
- - Server-side generator: `src/lib/setup/generate.ts`
223
- - CLI write orchestrator: `packages/cli/src/lib/write-configs.mjs`
@@ -1,213 +0,0 @@
1
- /**
2
- * CLI E2E tests for `skillrepo init`.
3
- *
4
- * Spins up a mock HTTP server, runs the real CLI binary as a subprocess,
5
- * and verifies the generated setup actually works — not just that files exist.
6
- *
7
- * Uses node:test + node:assert — zero external dependencies.
8
- *
9
- * IMPORTANT: The CLI is run via async `execFile` (not `execFileSync`).
10
- * execFileSync would block the Node event loop and starve the mock HTTP
11
- * server running in the same process, causing the CLI's fetch to hang.
12
- */
13
-
14
- import { describe, it, before, after, beforeEach, afterEach } from "node:test";
15
- import assert from "node:assert/strict";
16
- import {
17
- mkdtempSync, mkdirSync, rmSync, readFileSync, writeFileSync,
18
- existsSync,
19
- } from "node:fs";
20
- import { join, resolve, dirname } from "node:path";
21
- import { tmpdir } from "node:os";
22
- import { execFile } from "node:child_process";
23
- import { fileURLToPath } from "node:url";
24
-
25
- import { createMockServer } from "./mock-server.mjs";
26
- import { buildPayload } from "./payload-factory.mjs";
27
-
28
- const __dirname = dirname(fileURLToPath(import.meta.url));
29
- const CLI_BIN = resolve(__dirname, "../../../bin/skillrepo.mjs");
30
- const API_KEY = "sk_live_test123";
31
-
32
- // ---------------------------------------------------------------------------
33
- // Helpers
34
- // ---------------------------------------------------------------------------
35
-
36
- /**
37
- * Run the CLI init command against a mock server in a given cwd.
38
- * Returns a promise that resolves with stdout on success.
39
- */
40
- function runInit(cwd, port, { key = API_KEY, extraArgs = [] } = {}) {
41
- return new Promise((resolve, reject) => {
42
- execFile(
43
- process.execPath,
44
- [CLI_BIN, "init", "--url", `http://127.0.0.1:${port}`, "--key", key, "--yes", ...extraArgs],
45
- { cwd, encoding: "utf-8", timeout: 15_000, env: { ...process.env, NODE_NO_WARNINGS: "1" } },
46
- (err, stdout, stderr) => {
47
- if (err) return reject(Object.assign(err, { stdout, stderr }));
48
- resolve(stdout);
49
- },
50
- );
51
- });
52
- }
53
-
54
- /** Run the CLI and expect it to fail. Returns { stdout, stderr, status }. */
55
- function runInitExpectFail(cwd, port, { key = API_KEY } = {}) {
56
- return new Promise((resolve) => {
57
- execFile(
58
- process.execPath,
59
- [CLI_BIN, "init", "--url", `http://127.0.0.1:${port}`, "--key", key, "--yes"],
60
- { cwd, encoding: "utf-8", timeout: 15_000, env: { ...process.env, NODE_NO_WARNINGS: "1" } },
61
- (err, stdout, stderr) => {
62
- if (err) {
63
- resolve({ stdout: stdout ?? "", stderr: stderr ?? "", status: err.code ?? 1 });
64
- } else {
65
- resolve({ stdout, stderr: "", status: 0 });
66
- }
67
- },
68
- );
69
- });
70
- }
71
-
72
- /** Read a file from the temp dir. */
73
- function readFile(dir, relPath) {
74
- return readFileSync(join(dir, relPath), "utf-8");
75
- }
76
-
77
- /** Check if a file exists in the temp dir. */
78
- function fileExists(dir, relPath) {
79
- return existsSync(join(dir, relPath));
80
- }
81
-
82
- /** Parse JSON from a file in the temp dir. */
83
- function readJSON(dir, relPath) {
84
- return JSON.parse(readFile(dir, relPath));
85
- }
86
-
87
- /** Create a fresh temp dir with .claude/ and .cursor/ markers. */
88
- function makeTempDir() {
89
- const dir = mkdtempSync(join(tmpdir(), "cli-e2e-"));
90
- mkdirSync(join(dir, ".claude"), { recursive: true });
91
- mkdirSync(join(dir, ".cursor"), { recursive: true });
92
- return dir;
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // Test suite
97
- // ---------------------------------------------------------------------------
98
-
99
- describe("CLI E2E: skillrepo init", () => {
100
- let server;
101
- let port;
102
- let tempDir;
103
-
104
- before(async () => {
105
- server = createMockServer({});
106
- port = await server.start();
107
- const payload = buildPayload({ baseUrl: `http://127.0.0.1:${port}` });
108
- server.setPayload(payload);
109
- });
110
-
111
- after(async () => {
112
- if (server) await server.stop();
113
- });
114
-
115
- beforeEach(() => {
116
- tempDir = makeTempDir();
117
- });
118
-
119
- afterEach(() => {
120
- if (tempDir) rmSync(tempDir, { recursive: true, force: true });
121
- });
122
-
123
- // =========================================================================
124
- // 1. MCP Config
125
- // =========================================================================
126
-
127
- it(".mcp.json is created with MCP server config", async () => {
128
- await runInit(tempDir, port);
129
-
130
- const mcp = readJSON(tempDir, ".mcp.json");
131
- assert.ok(mcp.mcpServers?.skillrepo, "Should have skillrepo MCP server entry");
132
- });
133
-
134
- it(".env.local contains the API key", async () => {
135
- await runInit(tempDir, port);
136
-
137
- const envContent = readFile(tempDir, ".env.local");
138
- assert.ok(envContent.includes(`SKILLREPO_ACCESS_KEY=${API_KEY}`));
139
- });
140
-
141
- // =========================================================================
142
- // 2. Error Handling
143
- // =========================================================================
144
-
145
- it("invalid API key (401) exits with error", async () => {
146
- const srv = createMockServer({}, { validKey: "sk_live_onlythisone" });
147
- const p = await srv.start();
148
- const payload = buildPayload({ baseUrl: `http://127.0.0.1:${p}` });
149
- srv.setPayload(payload);
150
-
151
- try {
152
- const result = await runInitExpectFail(tempDir, p, { key: "sk_live_wrongkey" });
153
- assert.notEqual(result.status, 0);
154
- const output = result.stdout + result.stderr;
155
- assert.ok(output.toLowerCase().includes("invalid") || output.toLowerCase().includes("error"));
156
- } finally {
157
- await srv.stop();
158
- }
159
- });
160
-
161
- it("unreachable server exits with error", async () => {
162
- const result = await runInitExpectFail(tempDir, 19999);
163
- assert.notEqual(result.status, 0);
164
- const output = result.stdout + result.stderr;
165
- assert.ok(
166
- output.toLowerCase().includes("cannot reach") ||
167
- output.toLowerCase().includes("error") ||
168
- output.toLowerCase().includes("econnrefused"),
169
- );
170
- });
171
-
172
- // =========================================================================
173
- // 3. Generated Files Are Structurally Valid
174
- // =========================================================================
175
-
176
- it("all JSON files parse without error", async () => {
177
- await runInit(tempDir, port);
178
-
179
- for (const f of [".mcp.json"]) {
180
- assert.doesNotThrow(() => readJSON(tempDir, f), `Invalid JSON: ${f}`);
181
- }
182
- });
183
-
184
- // =========================================================================
185
- // 4. .gitignore and old format cleanup
186
- // =========================================================================
187
-
188
- it("old format files are cleaned up during migration", async () => {
189
- // Create old format files before init
190
- writeFileSync(join(tempDir, ".claude/skillrepo.md"), "# Old skillrepo md");
191
- writeFileSync(join(tempDir, ".claude/skillrepo-index.json"), '{"version":1}');
192
- writeFileSync(join(tempDir, ".claude/skillrepo-config.json"), '{"version":2}');
193
-
194
- await runInit(tempDir, port);
195
-
196
- // Old format files should be removed
197
- assert.ok(!fileExists(tempDir, ".claude/skillrepo.md"), "Old .claude/skillrepo.md should be removed");
198
- assert.ok(!fileExists(tempDir, ".claude/skillrepo-index.json"), "Old .claude/skillrepo-index.json should be removed");
199
- assert.ok(!fileExists(tempDir, ".claude/skillrepo-config.json"), "Old .claude/skillrepo-config.json should be removed");
200
- });
201
-
202
- // =========================================================================
203
- // 5. No hook files generated (hooks removed in v1)
204
- // =========================================================================
205
-
206
- it("does not generate hook files", async () => {
207
- await runInit(tempDir, port);
208
-
209
- assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "Sync hook should not exist");
210
- assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "Prompt-match hook should not exist");
211
- assert.ok(!fileExists(tempDir, ".claude/hooks/skillrepo-pretool-activation.mjs"), "PreToolUse hook should not exist");
212
- });
213
- });
@@ -1,22 +0,0 @@
1
- /**
2
- * Factory for SetupPayload objects used in CLI E2E tests.
3
- *
4
- * Phase D (#534): The setup endpoint now returns only `skillCount` and
5
- * `mcpUrl`. All hook/rule/config generation was removed — standalone
6
- * scripts bundled with the CLI handle everything.
7
- */
8
-
9
- /**
10
- * Build a SetupPayload for E2E tests.
11
- *
12
- * @param {object} [options]
13
- * @param {string} [options.baseUrl] - The base URL for the mock server
14
- * @returns {object} A SetupPayload
15
- */
16
- export function buildPayload(options = {}) {
17
- const baseUrl = options.baseUrl ?? "http://localhost:9999";
18
- return {
19
- skillCount: 5,
20
- mcpUrl: `${baseUrl}/api/mcp`,
21
- };
22
- }