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.
@@ -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 the `~/.acp` directory and use it to populate the upstream repo list if the environment variable is not set. This file should contain the same JSON array format as `ACP_REPOS_JSON` and is useful for per-user configuration without exporting environment variables. Precedence when building the repos list is:
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
- 1. `ACP_REPOS_JSON` environment variable (highest priority)
158
- 2. `~/.acp/acp-repos.json` file (if present)
159
- 3. Built-in default repo (github/awesome-copilot)
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
- Note: when running in development or tests the CLI will attempt to read `acp-repos.json` from the current working directory instead of `~/.acp` to keep test fixtures and local development predictable.
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
 
@@ -5,6 +5,7 @@ function makeCli() {
5
5
  _action: null,
6
6
  command() { return cli; },
7
7
  option() { return cli; },
8
+ example() { return cli; },
8
9
  action(fn) { cli._action = fn; return cli; }
9
10
  };
10
11
  return cli;
@@ -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
+ });