acp-vscode 0.3.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/copilot-instructions.md +80 -0
- package/README.md +25 -5
- package/__tests__/commands-actions.test.js +1 -0
- package/__tests__/fetcher-nested-and-default.test.js +461 -0
- package/__tests__/fetcher-verbose-integration.test.js +274 -0
- package/__tests__/fetcher-verbose.test.js +316 -0
- package/__tests__/fetcher.test.js +5 -5
- package/__tests__/installer-skills.test.js +266 -0
- package/__tests__/search-index.test.js +1 -1
- package/package.json +1 -1
- package/scripts/quality-checks.sh +93 -0
- package/src/commands/install.js +58 -25
- package/src/commands/list.js +1 -1
- package/src/commands/search.js +5 -3
- package/src/fetcher.js +242 -39
- package/src/installer.js +310 -8
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Copilot Instructions for acp-vscode
|
|
2
|
+
|
|
3
|
+
These instructions guide AI assistants (like GitHub Copilot) in maintaining code quality and best practices for the acp-vscode project.
|
|
4
|
+
|
|
5
|
+
## Quality Checks
|
|
6
|
+
|
|
7
|
+
After making any major changes to the codebase, **you must run the quality checks script and ensure all checks pass**:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
./scripts/quality-checks.sh
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This script runs:
|
|
14
|
+
- **ESLint** - Validates code style and best practices
|
|
15
|
+
- **Jest Tests** - Ensures all unit tests pass
|
|
16
|
+
- **package.json Validation** - Verifies the package configuration is valid
|
|
17
|
+
|
|
18
|
+
### When to Run Quality Checks
|
|
19
|
+
|
|
20
|
+
Run quality checks after:
|
|
21
|
+
- Implementing new features
|
|
22
|
+
- Fixing bugs
|
|
23
|
+
- Refactoring code
|
|
24
|
+
- Adding or modifying tests
|
|
25
|
+
- Updating dependencies
|
|
26
|
+
- Making any changes to source files
|
|
27
|
+
|
|
28
|
+
### Fixing Quality Check Failures
|
|
29
|
+
|
|
30
|
+
If any quality check fails:
|
|
31
|
+
|
|
32
|
+
1. **Read the error output carefully** - Identify the specific files and line numbers that failed
|
|
33
|
+
2. **Fix the issues incrementally** - Make small, focused changes to address each failure
|
|
34
|
+
3. **Re-run the quality checks** - After each fix, run the quality checks again to verify the change
|
|
35
|
+
4. **Iterate until all checks pass** - Continue fixing and testing until `./scripts/quality-checks.sh` exits with code 0
|
|
36
|
+
|
|
37
|
+
### ESLint Failures
|
|
38
|
+
|
|
39
|
+
If ESLint fails:
|
|
40
|
+
- Review the linting rules in the project configuration
|
|
41
|
+
- Fix code style issues (indentation, spacing, naming conventions)
|
|
42
|
+
- Follow the existing code style patterns in the codebase
|
|
43
|
+
- Many ESLint errors can be automatically fixed with: `npm run lint -- --fix`
|
|
44
|
+
|
|
45
|
+
### Jest Test Failures
|
|
46
|
+
|
|
47
|
+
If tests fail:
|
|
48
|
+
- Run the test suite with: `npm test`
|
|
49
|
+
- Review the test output to understand what assertions failed
|
|
50
|
+
- Update or add tests to cover the new code
|
|
51
|
+
- Ensure backward compatibility with existing tests
|
|
52
|
+
- Do not disable or skip tests to make them pass
|
|
53
|
+
|
|
54
|
+
## Code Quality Standards
|
|
55
|
+
|
|
56
|
+
### Guidelines
|
|
57
|
+
|
|
58
|
+
- **Consistency** - Follow the existing code style and patterns in the codebase
|
|
59
|
+
- **Testing** - All new features should have corresponding tests
|
|
60
|
+
- **Documentation** - Update README.md and other documentation as needed
|
|
61
|
+
- **Git Hygiene** - Make atomic, well-described commits
|
|
62
|
+
|
|
63
|
+
### Before Submitting Changes
|
|
64
|
+
|
|
65
|
+
1. Run `./scripts/quality-checks.sh` and ensure all checks pass
|
|
66
|
+
2. Verify no new warnings or errors were introduced
|
|
67
|
+
3. Test the changes manually if applicable
|
|
68
|
+
4. Review the changes for clarity and correctness
|
|
69
|
+
|
|
70
|
+
## References
|
|
71
|
+
|
|
72
|
+
- CI/CD Pipeline: [.github/workflows/ci.yml](./workflows/ci.yml)
|
|
73
|
+
- Release Process: [scripts/release.sh](../scripts/release.sh)
|
|
74
|
+
- Quality Checks: [scripts/quality-checks.sh](../scripts/quality-checks.sh)
|
|
75
|
+
|
|
76
|
+
## Related Configuration Files
|
|
77
|
+
|
|
78
|
+
- [package.json](../../package.json) - Project metadata and scripts
|
|
79
|
+
- [jest.config.cjs](../../jest.config.cjs) - Jest test configuration
|
|
80
|
+
- [.eslintrc or similar]() - ESLint configuration (if present)
|
package/README.md
CHANGED
|
@@ -102,6 +102,9 @@ acp-vscode list --refresh
|
|
|
102
102
|
- Offline testing / injecting a local index
|
|
103
103
|
- For tests or offline usage you can set `ACP_INDEX_JSON` to a JSON string representing the index. This bypasses network fetching entirely and the CLI will use the provided index verbatim.
|
|
104
104
|
|
|
105
|
+
- Nested folder support
|
|
106
|
+
- The CLI automatically scans for prompts, agents, instructions, and skills at any folder depth. Items can be organized at the root level (`prompts/`, `agents/`, etc.) or in nested folders like `.github/prompts/`, `workflows/prompts/`, etc.
|
|
107
|
+
|
|
105
108
|
- Multiple upstream repos
|
|
106
109
|
- To index multiple repos set `ACP_REPOS_JSON` to a JSON array of repo descriptors. Example:
|
|
107
110
|
|
|
@@ -138,6 +141,8 @@ ACP_REPOS_JSON
|
|
|
138
141
|
|
|
139
142
|
To support multiple upstream repos, set `ACP_REPOS_JSON` to a JSON array describing the repositories to index. Each repo object should contain at least an `id` and a `treeUrl`. Optionally include `rawBase` (the base URL to fetch raw file contents).
|
|
140
143
|
|
|
144
|
+
**Important:** The default awesome-copilot repo is always included alongside your configured repos unless you explicitly set `ACP_EXCLUDE_DEFAULT_REPO=true` (environment variable). This means your custom repos are *additive* to the default, not replacements.
|
|
145
|
+
|
|
141
146
|
Example:
|
|
142
147
|
|
|
143
148
|
```json
|
|
@@ -152,13 +157,28 @@ When multiple repos contain files with the same `id`, the fetcher adds an `_conf
|
|
|
152
157
|
Local repo file (acp-repos.json)
|
|
153
158
|
-------------------------------
|
|
154
159
|
|
|
155
|
-
In addition to `ACP_REPOS_JSON` the CLI will look for a file named `acp-repos.json` in
|
|
160
|
+
In addition to `ACP_REPOS_JSON` the CLI will look for a file named `acp-repos.json` in a local configuration directory and use it to populate the upstream repo list if the environment variable is not set. In normal usage (when `NODE_ENV` is not `development` or `test`), this file is expected at `~/.acp/acp-repos.json`. When `NODE_ENV` is set to `development` or `test`, the CLI instead looks for `./acp-repos.json` in the current working directory; this behavior exists primarily to support local development and automated tests. This file should contain the same JSON array format as `ACP_REPOS_JSON` and is useful for per-user configuration without exporting environment variables. **Like `ACP_REPOS_JSON`, the default awesome-copilot repo is always included alongside repos from this file unless you set `ACP_EXCLUDE_DEFAULT_REPO=true`.** Precedence when building the repos list is:
|
|
161
|
+
|
|
162
|
+
1. `ACP_REPOS_JSON` environment variable (highest priority) — default repo is always added
|
|
163
|
+
2. Local `acp-repos.json` file (if `ACP_REPOS_JSON` not set): in `~/.acp/acp-repos.json` by default, or `./acp-repos.json` when `NODE_ENV` is `development` or `test` — default repo is always added
|
|
164
|
+
3. Built-in default repo (github/awesome-copilot) — used as fallback
|
|
165
|
+
|
|
166
|
+
To exclude the default awesome-copilot repo entirely, set the `ACP_EXCLUDE_DEFAULT_REPO` environment variable to `true` or `1`.
|
|
167
|
+
|
|
168
|
+
ACP_EXCLUDE_DEFAULT_REPO
|
|
156
169
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
By default, the awesome-copilot repository is always included in the index regardless of whether you've configured custom repos via `ACP_REPOS_JSON` or `acp-repos.json`. This makes custom repos *additive* rather than replacements.
|
|
171
|
+
|
|
172
|
+
If you want to exclude the default awesome-copilot repo, set `ACP_EXCLUDE_DEFAULT_REPO=true` or `ACP_EXCLUDE_DEFAULT_REPO=1`:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
# Exclude default repo and use only custom repos
|
|
176
|
+
export ACP_EXCLUDE_DEFAULT_REPO=true
|
|
177
|
+
export ACP_REPOS_JSON='[{"id":"myrepo","treeUrl":"...","rawBase":"..."}]'
|
|
178
|
+
acp-vscode list
|
|
179
|
+
```
|
|
160
180
|
|
|
161
|
-
|
|
181
|
+
When this is set to any value other than `true` or `1`, the default repo will be included (default behavior).
|
|
162
182
|
|
|
163
183
|
Dry-run:
|
|
164
184
|
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
jest.mock('axios');
|
|
6
|
+
|
|
7
|
+
let fetcher;
|
|
8
|
+
let fetchIndex;
|
|
9
|
+
let diskPaths;
|
|
10
|
+
const cache = require('../src/cache');
|
|
11
|
+
|
|
12
|
+
describe('fetcher nested folders and default repo behavior', () => {
|
|
13
|
+
const tmp = path.join(os.tmpdir(), `acp-fetcher-nested-${Date.now()}`);
|
|
14
|
+
const origCwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
await fs.ensureDir(tmp);
|
|
18
|
+
process.chdir(tmp);
|
|
19
|
+
process.env.NODE_ENV = 'test';
|
|
20
|
+
// require after chdir so diskPaths uses tmp cwd
|
|
21
|
+
fetcher = require('../src/fetcher');
|
|
22
|
+
fetchIndex = fetcher.fetchIndex;
|
|
23
|
+
diskPaths = fetcher.diskPaths;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterAll(async () => {
|
|
27
|
+
process.chdir(origCwd);
|
|
28
|
+
await fs.remove(tmp);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
cache.del('index');
|
|
33
|
+
const { DISK_CACHE_DIR } = diskPaths();
|
|
34
|
+
await fs.remove(DISK_CACHE_DIR).catch(() => {});
|
|
35
|
+
// Clean up acp-repos.json file from previous tests
|
|
36
|
+
const baseDir = process.cwd();
|
|
37
|
+
const repoFile = path.join(baseDir, 'acp-repos.json');
|
|
38
|
+
await fs.remove(repoFile).catch(() => {});
|
|
39
|
+
axios.get.mockReset();
|
|
40
|
+
// Clear env vars
|
|
41
|
+
delete process.env.ACP_INDEX_JSON;
|
|
42
|
+
delete process.env.ACP_REPOS_JSON;
|
|
43
|
+
delete process.env.ACP_EXCLUDE_DEFAULT_REPO;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('scans .github folder for prompts alongside root-level prompts', async () => {
|
|
47
|
+
const testRepoConfig = [
|
|
48
|
+
{
|
|
49
|
+
id: 'test-repo',
|
|
50
|
+
treeUrl: 'https://api.test/tree',
|
|
51
|
+
rawBase: 'https://raw.test'
|
|
52
|
+
}
|
|
53
|
+
];
|
|
54
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
55
|
+
|
|
56
|
+
axios.get.mockImplementation((url) => {
|
|
57
|
+
if (url === 'https://api.test/tree') {
|
|
58
|
+
return Promise.resolve({
|
|
59
|
+
status: 200,
|
|
60
|
+
data: {
|
|
61
|
+
tree: [
|
|
62
|
+
// Root level prompts
|
|
63
|
+
{ type: 'blob', path: 'prompts/root-prompt.prompt.md' },
|
|
64
|
+
// .github folder prompts
|
|
65
|
+
{ type: 'blob', path: '.github/prompts/github-prompt.prompt.md' },
|
|
66
|
+
// Root level agents
|
|
67
|
+
{ type: 'blob', path: 'agents/root-agent.agent.md' },
|
|
68
|
+
// .github folder agents
|
|
69
|
+
{ type: 'blob', path: '.github/agents/github-agent.agent.md' }
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Mock content fetches
|
|
75
|
+
if (url.includes('raw.test')) {
|
|
76
|
+
return Promise.resolve({
|
|
77
|
+
status: 200,
|
|
78
|
+
data: '---\ntitle: "Test Item"\n---\nContent'
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const idx = await fetchIndex();
|
|
85
|
+
|
|
86
|
+
// Should find both root and .github prompts
|
|
87
|
+
expect(idx.prompts).toHaveLength(2);
|
|
88
|
+
const promptIds = idx.prompts.map(p => p.id).sort();
|
|
89
|
+
expect(promptIds).toContain('root-prompt');
|
|
90
|
+
expect(promptIds).toContain('github-prompt');
|
|
91
|
+
|
|
92
|
+
// Should find both root and .github agents
|
|
93
|
+
expect(idx.agents).toHaveLength(2);
|
|
94
|
+
const agentIds = idx.agents.map(a => a.id).sort();
|
|
95
|
+
expect(agentIds).toContain('root-agent');
|
|
96
|
+
expect(agentIds).toContain('github-agent');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('finds items in arbitrarily nested folder structures', async () => {
|
|
100
|
+
const testRepoConfig = [
|
|
101
|
+
{
|
|
102
|
+
id: 'test-repo',
|
|
103
|
+
treeUrl: 'https://api.test/tree',
|
|
104
|
+
rawBase: 'https://raw.test'
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
108
|
+
|
|
109
|
+
axios.get.mockImplementation((url) => {
|
|
110
|
+
if (url === 'https://api.test/tree') {
|
|
111
|
+
return Promise.resolve({
|
|
112
|
+
status: 200,
|
|
113
|
+
data: {
|
|
114
|
+
tree: [
|
|
115
|
+
// Various nesting levels
|
|
116
|
+
{ type: 'blob', path: 'prompts/p1.prompt.md' },
|
|
117
|
+
{ type: 'blob', path: '.github/prompts/p2.prompt.md' },
|
|
118
|
+
{ type: 'blob', path: 'workflows/prompts/p3.prompt.md' },
|
|
119
|
+
{ type: 'blob', path: 'docs/.github/prompts/p4.prompt.md' }
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (url.includes('raw.test')) {
|
|
125
|
+
return Promise.resolve({
|
|
126
|
+
status: 200,
|
|
127
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const idx = await fetchIndex();
|
|
134
|
+
|
|
135
|
+
// Should find all prompts regardless of nesting level
|
|
136
|
+
expect(idx.prompts).toHaveLength(4);
|
|
137
|
+
const promptIds = idx.prompts.map(p => p.id).sort();
|
|
138
|
+
expect(promptIds).toContain('p1');
|
|
139
|
+
expect(promptIds).toContain('p2');
|
|
140
|
+
expect(promptIds).toContain('p3');
|
|
141
|
+
expect(promptIds).toContain('p4');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('always includes default awesome-copilot repo when ACP_REPOS_JSON is set', async () => {
|
|
145
|
+
const customRepoConfig = [
|
|
146
|
+
{
|
|
147
|
+
id: 'custom-repo',
|
|
148
|
+
treeUrl: 'https://api.test/custom',
|
|
149
|
+
rawBase: 'https://raw.test/custom'
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(customRepoConfig);
|
|
153
|
+
|
|
154
|
+
axios.get.mockImplementation((url) => {
|
|
155
|
+
if (url.includes('test/custom')) {
|
|
156
|
+
return Promise.resolve({
|
|
157
|
+
status: 200,
|
|
158
|
+
data: { tree: [{ type: 'blob', path: 'prompts/custom.prompt.md' }] }
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (url.includes('.com/repos/github/awesome-copilot')) {
|
|
162
|
+
return Promise.resolve({
|
|
163
|
+
status: 200,
|
|
164
|
+
data: { tree: [{ type: 'blob', path: 'prompts/default.prompt.md' }] }
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (url.includes('raw.')) {
|
|
168
|
+
return Promise.resolve({
|
|
169
|
+
status: 200,
|
|
170
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const idx = await fetchIndex();
|
|
177
|
+
|
|
178
|
+
// Should have prompts from both custom and default repos
|
|
179
|
+
expect(idx.prompts.length).toBeGreaterThanOrEqual(2);
|
|
180
|
+
expect(idx._repos.some(r => r.id === 'custom-repo')).toBe(true);
|
|
181
|
+
expect(idx._repos.some(r => r.id === 'awesome-copilot')).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('always includes default awesome-copilot repo when acp-repos.json file is used', async () => {
|
|
185
|
+
const fileRepoConfig = [
|
|
186
|
+
{
|
|
187
|
+
id: 'file-repo',
|
|
188
|
+
treeUrl: 'https://api.test/file',
|
|
189
|
+
rawBase: 'https://raw.test/file'
|
|
190
|
+
}
|
|
191
|
+
];
|
|
192
|
+
const baseDir = process.cwd();
|
|
193
|
+
const repoFile = path.join(baseDir, 'acp-repos.json');
|
|
194
|
+
await fs.writeJson(repoFile, fileRepoConfig);
|
|
195
|
+
|
|
196
|
+
axios.get.mockImplementation((url) => {
|
|
197
|
+
if (url.includes('test/file')) {
|
|
198
|
+
return Promise.resolve({
|
|
199
|
+
status: 200,
|
|
200
|
+
data: { tree: [{ type: 'blob', path: 'prompts/file-item.prompt.md' }] }
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (url.includes('.com/repos/github/awesome-copilot')) {
|
|
204
|
+
return Promise.resolve({
|
|
205
|
+
status: 200,
|
|
206
|
+
data: { tree: [{ type: 'blob', path: 'prompts/default-item.prompt.md' }] }
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (url.includes('raw.')) {
|
|
210
|
+
return Promise.resolve({
|
|
211
|
+
status: 200,
|
|
212
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const idx = await fetchIndex();
|
|
219
|
+
|
|
220
|
+
// Should have repos from both file config and default
|
|
221
|
+
expect(idx._repos.some(r => r.id === 'file-repo')).toBe(true);
|
|
222
|
+
expect(idx._repos.some(r => r.id === 'awesome-copilot')).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('respects ACP_EXCLUDE_DEFAULT_REPO when set to "true"', async () => {
|
|
226
|
+
process.env.ACP_EXCLUDE_DEFAULT_REPO = 'true';
|
|
227
|
+
const customRepoConfig = [
|
|
228
|
+
{
|
|
229
|
+
id: 'custom-repo',
|
|
230
|
+
treeUrl: 'https://api.test/custom',
|
|
231
|
+
rawBase: 'https://raw.test/custom'
|
|
232
|
+
}
|
|
233
|
+
];
|
|
234
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(customRepoConfig);
|
|
235
|
+
|
|
236
|
+
axios.get.mockImplementation((url) => {
|
|
237
|
+
if (url.includes('test/custom')) {
|
|
238
|
+
return Promise.resolve({
|
|
239
|
+
status: 200,
|
|
240
|
+
data: { tree: [{ type: 'blob', path: 'prompts/custom.prompt.md' }] }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (url.includes('raw.test')) {
|
|
244
|
+
return Promise.resolve({
|
|
245
|
+
status: 200,
|
|
246
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// Should not be called for awesome-copilot
|
|
250
|
+
if (url.includes('.com/repos/github/awesome-copilot')) {
|
|
251
|
+
return Promise.reject(new Error('Should not fetch default repo when excluded'));
|
|
252
|
+
}
|
|
253
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const idx = await fetchIndex();
|
|
257
|
+
|
|
258
|
+
// Should only have custom repo, not default
|
|
259
|
+
expect(idx._repos).toHaveLength(1);
|
|
260
|
+
expect(idx._repos[0].id).toBe('custom-repo');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('respects ACP_EXCLUDE_DEFAULT_REPO when set to "1"', async () => {
|
|
264
|
+
process.env.ACP_EXCLUDE_DEFAULT_REPO = '1';
|
|
265
|
+
|
|
266
|
+
axios.get.mockImplementation((url) => {
|
|
267
|
+
// Default repo should not be called
|
|
268
|
+
if (url.includes('.com/repos/github/awesome-copilot')) {
|
|
269
|
+
return Promise.reject(new Error('Should not fetch default repo when excluded'));
|
|
270
|
+
}
|
|
271
|
+
// No other repos configured
|
|
272
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Should throw an error when no repos are configured and default is excluded
|
|
276
|
+
await expect(fetchIndex()).rejects.toThrow(
|
|
277
|
+
'No repos configured. When ACP_EXCLUDE_DEFAULT_REPO is set, you must configure at least one repo'
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('includes default repo even if already in ACP_REPOS_JSON to avoid duplicates', async () => {
|
|
282
|
+
const configWithDefault = [
|
|
283
|
+
{
|
|
284
|
+
id: 'awesome-copilot',
|
|
285
|
+
treeUrl: 'https://api.github.com/repos/github/awesome-copilot/git/trees/main?recursive=1',
|
|
286
|
+
rawBase: 'https://raw.githubusercontent.com/github/awesome-copilot/main'
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
id: 'custom-repo',
|
|
290
|
+
treeUrl: 'https://api.test/custom',
|
|
291
|
+
rawBase: 'https://raw.test/custom'
|
|
292
|
+
}
|
|
293
|
+
];
|
|
294
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(configWithDefault);
|
|
295
|
+
|
|
296
|
+
axios.get.mockImplementation((url) => {
|
|
297
|
+
if (url.includes('test/custom')) {
|
|
298
|
+
return Promise.resolve({
|
|
299
|
+
status: 200,
|
|
300
|
+
data: { tree: [{ type: 'blob', path: 'prompts/custom.prompt.md' }] }
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (url.includes('.com/repos/github/awesome-copilot')) {
|
|
304
|
+
return Promise.resolve({
|
|
305
|
+
status: 200,
|
|
306
|
+
data: { tree: [{ type: 'blob', path: 'prompts/default.prompt.md' }] }
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (url.includes('raw.')) {
|
|
310
|
+
return Promise.resolve({
|
|
311
|
+
status: 200,
|
|
312
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const idx = await fetchIndex();
|
|
319
|
+
|
|
320
|
+
// Should have two repos (awesome-copilot and custom), not duplicates
|
|
321
|
+
const repoIds = idx._repos.map(r => r.id);
|
|
322
|
+
expect(repoIds.filter(id => id === 'awesome-copilot')).toHaveLength(1);
|
|
323
|
+
expect(repoIds).toContain('custom-repo');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('verbose logging shows .github folder discovery', async () => {
|
|
327
|
+
const testRepoConfig = [
|
|
328
|
+
{
|
|
329
|
+
id: 'test-repo',
|
|
330
|
+
treeUrl: 'https://api.test/tree',
|
|
331
|
+
rawBase: 'https://raw.test'
|
|
332
|
+
}
|
|
333
|
+
];
|
|
334
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
335
|
+
|
|
336
|
+
const logs = [];
|
|
337
|
+
const originalLog = console.log;
|
|
338
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
axios.get.mockImplementation((url) => {
|
|
342
|
+
if (url === 'https://api.test/tree') {
|
|
343
|
+
return Promise.resolve({
|
|
344
|
+
status: 200,
|
|
345
|
+
data: {
|
|
346
|
+
tree: [
|
|
347
|
+
{ type: 'blob', path: 'prompts/root.prompt.md' },
|
|
348
|
+
{ type: 'blob', path: '.github/prompts/github.prompt.md' }
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
if (url.includes('raw.test')) {
|
|
354
|
+
return Promise.resolve({
|
|
355
|
+
status: 200,
|
|
356
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
await fetchIndex({ verbose: true });
|
|
363
|
+
|
|
364
|
+
// Should show that items were found at any depth
|
|
365
|
+
expect(logs.some(l => l.includes('searched at any depth'))).toBe(true);
|
|
366
|
+
expect(logs.some(l => l.includes('found 2 prompts items'))).toBe(true);
|
|
367
|
+
} finally {
|
|
368
|
+
console.log = originalLog;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('verbose logging shows default repo inclusion when ACP_REPOS_JSON is used', async () => {
|
|
373
|
+
const testRepoConfig = [
|
|
374
|
+
{
|
|
375
|
+
id: 'test-repo',
|
|
376
|
+
treeUrl: 'https://api.test/tree',
|
|
377
|
+
rawBase: 'https://raw.test'
|
|
378
|
+
}
|
|
379
|
+
];
|
|
380
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(testRepoConfig);
|
|
381
|
+
|
|
382
|
+
const logs = [];
|
|
383
|
+
const originalLog = console.log;
|
|
384
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
axios.get.mockImplementation((url) => {
|
|
388
|
+
if (url.includes('test/tree')) {
|
|
389
|
+
return Promise.resolve({
|
|
390
|
+
status: 200,
|
|
391
|
+
data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] }
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (url.includes('github.com/repos/github/awesome-copilot')) {
|
|
395
|
+
return Promise.resolve({
|
|
396
|
+
status: 200,
|
|
397
|
+
data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] }
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
if (url.includes('raw.')) {
|
|
401
|
+
return Promise.resolve({
|
|
402
|
+
status: 200,
|
|
403
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await fetchIndex({ verbose: true });
|
|
410
|
+
|
|
411
|
+
expect(logs.some(l => l.includes('including default repo: awesome-copilot'))).toBe(true);
|
|
412
|
+
expect(logs.some(l => l.includes('final repos list:'))).toBe(true);
|
|
413
|
+
expect(logs.some(l => l.includes('awesome-copilot'))).toBe(true);
|
|
414
|
+
expect(logs.some(l => l.includes('test-repo'))).toBe(true);
|
|
415
|
+
} finally {
|
|
416
|
+
console.log = originalLog;
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test('verbose logging shows default repo exclusion', async () => {
|
|
421
|
+
process.env.ACP_EXCLUDE_DEFAULT_REPO = 'true';
|
|
422
|
+
const customRepoConfig = [
|
|
423
|
+
{
|
|
424
|
+
id: 'custom-repo',
|
|
425
|
+
treeUrl: 'https://api.test/custom',
|
|
426
|
+
rawBase: 'https://raw.test/custom'
|
|
427
|
+
}
|
|
428
|
+
];
|
|
429
|
+
process.env.ACP_REPOS_JSON = JSON.stringify(customRepoConfig);
|
|
430
|
+
|
|
431
|
+
const logs = [];
|
|
432
|
+
const originalLog = console.log;
|
|
433
|
+
console.log = jest.fn(msg => logs.push(msg));
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
axios.get.mockImplementation((url) => {
|
|
437
|
+
if (url.includes('test/custom')) {
|
|
438
|
+
return Promise.resolve({
|
|
439
|
+
status: 200,
|
|
440
|
+
data: { tree: [{ type: 'blob', path: 'prompts/p.prompt.md' }] }
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
if (url.includes('raw.test')) {
|
|
444
|
+
return Promise.resolve({
|
|
445
|
+
status: 200,
|
|
446
|
+
data: '---\ntitle: "Test"\n---\nContent'
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return Promise.reject(new Error('Unexpected URL'));
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await fetchIndex({ verbose: true });
|
|
453
|
+
|
|
454
|
+
expect(logs.some(l => l.includes('default repo excluded via ACP_EXCLUDE_DEFAULT_REPO'))).toBe(true);
|
|
455
|
+
expect(logs.some(l => l.includes('final repos list:'))).toBe(true);
|
|
456
|
+
expect(logs.filter(l => l.includes('awesome-copilot')).length).toBe(0);
|
|
457
|
+
} finally {
|
|
458
|
+
console.log = originalLog;
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|