acp-vscode 0.3.2 → 0.3.8

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.
@@ -17,5 +17,7 @@ jobs:
17
17
  node-version: '20'
18
18
  - name: Install dependencies
19
19
  run: npm ci
20
+ - name: Run linting
21
+ run: npm run lint
20
22
  - name: Run tests
21
23
  run: npm test
@@ -5,9 +5,9 @@ name: Release
5
5
  # Then push the tag: `git push --follow-tags` (or `git push origin vX.Y.Z`).
6
6
  # - Alternatively, use a release manager like `semantic-release` to automate versioning and changelogs.
7
7
  # - This workflow runs when a tag matching `v*.*.*` is pushed. Ensure you create/tag using the v-prefixed semver format.
8
- # - Secrets required:
9
- # - `NPM_TOKEN` (used for npm publish)
10
- # - The default `GITHUB_TOKEN` is provided automatically by Actions for creating releases.
8
+ # - This workflow uses Trusted Publishing (OIDC) for npm - no NPM_TOKEN secret needed!
9
+ # - Provenance attestations are automatically generated when using trusted publishing.
10
+ # - The default `GITHUB_TOKEN` is provided automatically by Actions for creating releases.
11
11
 
12
12
  on:
13
13
  push:
@@ -21,6 +21,8 @@ jobs:
21
21
  permissions:
22
22
  contents: write # needed to create a release
23
23
  packages: write # not strictly required for npm but useful if you use GitHub Packages
24
+ id-token: write # Required for OIDC
25
+
24
26
  steps:
25
27
  # Fetch full history so tags and changelog generation work correctly
26
28
  - uses: actions/checkout@v4
@@ -29,15 +31,17 @@ jobs:
29
31
  - name: Use Node.js
30
32
  uses: actions/setup-node@v4
31
33
  with:
32
- node-version: '20'
34
+ node-version: '22'
33
35
  registry-url: 'https://registry.npmjs.org'
36
+ - name: Upgrade npm for trusted publishing
37
+ run: npm install -g npm@latest
34
38
  - name: Install
35
39
  run: npm ci
40
+ - name: Run linting
41
+ run: npm run lint
36
42
  - name: Run tests
37
43
  run: npm test
38
44
  - name: Publish to npm
39
- env:
40
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
41
45
  run: npm publish
42
46
  - name: Create GitHub Release
43
47
  # Create a GitHub Release for the pushed tag. Uses the git ref by default.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # acp-vscode
2
2
 
3
- acp-vscode is a small CLI to fetch and install chatmodes, prompts and instructions from the GitHub "awesome-copilot" repository into your VS Code workspace or VS Code User profile.
3
+ acp-vscode is a small CLI to fetch and install agents, prompts, instructions and skills from the GitHub "awesome-copilot" repository into your VS Code workspace or VS Code User profile.
4
4
 
5
5
  Install (when published):
6
6
 
@@ -14,9 +14,9 @@ Commands:
14
14
  - install <workspace|user> [names...]
15
15
  - target: `workspace` or `user`
16
16
  - names: optional list of ids or names to install (supports `repo:id` form)
17
- - type: specify with the option `--type <type>` (prompts|chatmodes|instructions|all). For backwards compatibility you can still pass the type as the first positional name (e.g. `install workspace prompts p1 p2`). Note: the `install` command also accepts a deliberate typo alias `--referesh` (alias for `--refresh`) to preserve historical behavior.
17
+ - type: specify with the option `--type <type>` (prompts|agents|instructions|skills|chatmodes|all). Note: `chatmodes` is legacy/deprecated (use `agents` instead). For backwards compatibility you can still pass the type as the first positional name (e.g. `install workspace prompts p1 p2`). The `install` command also accepts a deliberate typo alias `--referesh` (alias for `--refresh`) to preserve historical behavior.
18
18
  - list [type]
19
- - list items available. type can be `prompts`, `chatmodes`, `instructions`, or `all`
19
+ - list items available. type can be `prompts`, `agents`, `instructions`, `skills`, `chatmodes` (legacy), or `all`
20
20
  - search <query>
21
21
  - search across items
22
22
  - uninstall <workspace|user> <type> [names...]
@@ -92,7 +92,7 @@ Troubleshooting
92
92
  ---------------
93
93
 
94
94
  - Cache and stale index
95
- - The CLI writes a disk cache at `./.acp-cache/index.json` (30 minute TTL). If you see stale results or want to force a fresh fetch, remove the cache file and retry:
95
+ - By default the CLI writes a disk cache under the user's home directory in `~/.acp/cache/index.json` (30 minute TTL). When running in development or tests the CLI preserves the old behavior and keeps the cache in the current working directory at `./.acp-cache/index.json` to avoid surprising local workflows. If you see stale results or want to force a fresh fetch, remove the cache file and retry:
96
96
 
97
97
  ```bash
98
98
  rm -rf .acp-cache
@@ -126,7 +126,9 @@ You can inject a full, pre-built index via the `ACP_INDEX_JSON` environment vari
126
126
  {
127
127
  "prompts": [{ "id": "p1", "name": "Prompt 1", "repo": "r1", "url": "https://..." }],
128
128
  "chatmodes": [],
129
- "instructions": []
129
+ "agents": [],
130
+ "instructions": [],
131
+ "skills": []
130
132
  }
131
133
  ```
132
134
 
@@ -147,6 +149,17 @@ Example:
147
149
 
148
150
  When multiple repos contain files with the same `id`, the fetcher adds an `_conflicts` array to the returned index listing conflicted ids. Consumers will display items as `repo:id` when necessary to disambiguate.
149
151
 
152
+ Local repo file (acp-repos.json)
153
+ -------------------------------
154
+
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:
156
+
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)
160
+
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.
162
+
150
163
  Dry-run:
151
164
 
152
165
  You can preview what would be installed without writing files using --dry-run:
@@ -168,8 +181,8 @@ Commands reference
168
181
  Short reference for each command, key options, and quick examples.
169
182
 
170
183
  - install <workspace|user> [names...]
171
- - Description: Install prompts/chatmodes/instructions into a workspace or VS Code user profile.
172
- - Options: `-t, --type <type>` (prompts|chatmodes|instructions|all), `--dry-run`, `--referesh` (alias for refresh), `--verbose`
184
+ - Description: Install prompts/agents/instructions/skills into a workspace or VS Code user profile.
185
+ - Options: `-t, --type <type>` (prompts|agents|instructions|skills|chatmodes|all; `chatmodes` is legacy/deprecated), `--dry-run`, `--referesh` (alias for refresh), `--verbose`
173
186
  - Examples:
174
187
  - Install all prompts into the current workspace:
175
188
  - `acp-vscode install workspace prompts`
@@ -177,10 +190,10 @@ Short reference for each command, key options, and quick examples.
177
190
  - `acp-vscode install user --type instructions "Instruction Name"`
178
191
 
179
192
  - list [type]
180
- - Description: List available items. Type can be `prompts`, `chatmodes`, `instructions`, or `all` (default).
193
+ - Description: List available items. Type can be `prompts`, `agents`, `instructions`, `skills`, `chatmodes` (legacy), or `all` (default).
181
194
  - Options: `-r, --refresh` (clear caches and refetch), `-j, --json`, `--verbose`
182
195
  - Examples:
183
- - `acp-vscode list chatmodes`
196
+ - `acp-vscode list agents`
184
197
  - `acp-vscode list --json`
185
198
 
186
199
  - search <query>
@@ -1,6 +1,4 @@
1
1
  const cache = require('../src/cache');
2
- const fs = require('fs-extra');
3
- const path = require('path');
4
2
 
5
3
  function makeCli() {
6
4
  const cli = {
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const os = require('os');
4
- const axios = require('axios');
5
4
  jest.mock('axios');
6
5
 
7
6
  let performInstall;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "acp-vscode",
3
- "version": "0.3.2",
4
- "description": "CLI to install GitHub Awesome Copilot chatmodes, prompts, and instructions into VS Code workspace or user profile",
3
+ "version": "0.3.8",
4
+ "description": "CLI to install GitHub Awesome Copilot agents, prompts, instructions, and skills into VS Code workspace or user profile",
5
5
  "bin": {
6
6
  "acp-vscode": "./bin/acp-vscode.js"
7
7
  },
@@ -0,0 +1,89 @@
1
+ #!/bin/bash
2
+
3
+ # Release script for acp-vscode
4
+ # Usage: ./scripts/release.sh <major|minor|patch>
5
+ # Example: ./scripts/release.sh minor
6
+
7
+ set -e
8
+
9
+ # Colors for output
10
+ RED='\033[0;31m'
11
+ GREEN='\033[0;32m'
12
+ YELLOW='\033[1;33m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Validate input
16
+ if [ $# -ne 1 ]; then
17
+ echo -e "${RED}Error: Release type required${NC}"
18
+ echo "Usage: ./scripts/release.sh <major|minor|patch>"
19
+ exit 1
20
+ fi
21
+
22
+ VERSION_TYPE=$1
23
+
24
+ # Validate version type
25
+ if [[ ! $VERSION_TYPE =~ ^(major|minor|patch)$ ]]; then
26
+ echo -e "${RED}Error: Invalid release type '${VERSION_TYPE}'${NC}"
27
+ echo "Valid options: major, minor, patch"
28
+ exit 1
29
+ fi
30
+
31
+ echo -e "${YELLOW}Starting release process...${NC}"
32
+
33
+ # Check if working directory is clean
34
+ if [ -n "$(git status --porcelain)" ]; then
35
+ echo -e "${RED}Error: Working directory is not clean (uncommitted or untracked changes)${NC}"
36
+ echo "Please commit or stash your changes before releasing."
37
+ git status --short
38
+ exit 1
39
+ fi
40
+
41
+ echo -e "${GREEN}✓ Working directory is clean${NC}"
42
+
43
+ # Run tests
44
+ echo -e "${YELLOW}Running tests...${NC}"
45
+ npm test || {
46
+ echo -e "${RED}Error: Tests failed${NC}"
47
+ exit 1
48
+ }
49
+ echo -e "${GREEN}✓ Tests passed${NC}"
50
+
51
+ # Run linting if available
52
+ if node -p "require('./package.json').scripts?.lint" 2>/dev/null | grep -v "undefined" >/dev/null; then
53
+ echo -e "${YELLOW}Running lint...${NC}"
54
+ if npm run lint; then
55
+ echo -e "${GREEN}✓ Linting passed${NC}"
56
+ else
57
+ echo -e "${RED}Error: Linting failed${NC}"
58
+ exit 1
59
+ fi
60
+ else
61
+ echo -e "${YELLOW}⚠ Linting not available (skipping)${NC}"
62
+ fi
63
+
64
+ # Display current version
65
+ CURRENT_VERSION=$(node -p "require('./package.json').version")
66
+ echo -e "${YELLOW}Current version: ${CURRENT_VERSION}${NC}"
67
+
68
+ # Use npm version to bump version and create tag
69
+ echo -e "${YELLOW}Bumping version (${VERSION_TYPE})...${NC}"
70
+ npm version "$VERSION_TYPE"
71
+
72
+ # Get the new version
73
+ NEW_VERSION=$(node -p "require('./package.json').version")
74
+ echo -e "${GREEN}✓ Version bumped to ${NEW_VERSION}${NC}"
75
+
76
+ # Get the tag that was created
77
+ TAG="v${NEW_VERSION}"
78
+
79
+ # Push the tag to remote
80
+ echo -e "${YELLOW}Pushing tag ${TAG} to remote...${NC}"
81
+ git push --follow-tags || {
82
+ echo -e "${RED}Error: Failed to push tag${NC}"
83
+ echo "You may need to push manually with: git push --follow-tags"
84
+ exit 1
85
+ }
86
+
87
+ echo -e "${GREEN}✓ Tag pushed successfully${NC}"
88
+ echo -e "${GREEN}Release complete! Tag ${TAG} has been pushed.${NC}"
89
+ echo -e "${YELLOW}The GitHub Actions workflow will now build and publish the release.${NC}"
@@ -1,5 +1,5 @@
1
1
  const prompts = require('prompts');
2
- const { fetchIndex, diskPaths } = require('../fetcher');
2
+ const { fetchIndex, diskPaths, cacheExists } = require('../fetcher');
3
3
  const cache = require('../cache');
4
4
  const { installFiles } = require('../installer');
5
5
  const fs = require('fs-extra');
@@ -8,7 +8,7 @@ async function performInstall({ target, type, names, options, workspaceDir = pro
8
8
  // support shorthand: if first arg is a package name (not 'workspace'|'user'),
9
9
  // treat it as package-mode and default target to 'workspace'.
10
10
  const key = 'index';
11
- const TYPES = ['prompts','chatmodes','instructions','all'];
11
+ const TYPES = ['prompts','chatmodes','agents','instructions','skills','all'];
12
12
  const isTargetValid = (target === 'workspace' || target === 'user');
13
13
  let packageMode = false;
14
14
  if (!isTargetValid) {
@@ -40,6 +40,13 @@ async function performInstall({ target, type, names, options, workspaceDir = pro
40
40
  let index = cache.get(key);
41
41
  if (!index) {
42
42
  try {
43
+ // Check if disk cache exists to determine if this is a fresh fetch
44
+ const diskCacheExists = await cacheExists();
45
+
46
+ if (!diskCacheExists && !doRefresh) {
47
+ console.log('Updating cache for the first time... (use --refresh/--referesh to force refresh)');
48
+ }
49
+
43
50
  index = await fetchIndex();
44
51
  cache.set(key, index);
45
52
  } catch (err) {
@@ -48,7 +55,7 @@ async function performInstall({ target, type, names, options, workspaceDir = pro
48
55
  }
49
56
  }
50
57
 
51
- const types = (type === 'all' || !type) ? ['prompts','chatmodes','instructions'] : [type];
58
+ const types = (type === 'all' || !type) ? ['prompts','chatmodes','agents','instructions','skills'] : [type];
52
59
  let anyFound = false;
53
60
  const missingNames = new Set();
54
61
  // If multiple types are being considered and names provided, resolve names across types
@@ -219,8 +226,8 @@ async function performInstall({ target, type, names, options, workspaceDir = pro
219
226
  continue;
220
227
  }
221
228
 
222
- // If no specific names requested and the type is chatmodes, prompts, or instructions, confirm installing all
223
- if ((!names || names.length === 0) && (t === 'chatmodes' || t === 'prompts' || t === 'instructions')) {
229
+ // If no specific names requested and the type is chatmodes, agents, prompts, instructions or skills, confirm installing all
230
+ if ((!names || names.length === 0) && (t === 'chatmodes' || t === 'agents' || t === 'prompts' || t === 'instructions' || t === 'skills')) {
224
231
  // When running non-interactively (no TTY), auto-confirm so CI/tests proceed
225
232
  const nonInteractive = !(process.stdin && process.stdin.isTTY);
226
233
  if (!nonInteractive) {
@@ -252,15 +259,15 @@ async function performInstall({ target, type, names, options, workspaceDir = pro
252
259
  function installCommand(cli) {
253
260
  // Change signature so that names are variadic and type is provided via option to
254
261
  // avoid the ambiguity where the second positional arg would be parsed as `type`.
255
- cli.command('install <target> [names...]', 'Install items into workspace or user profile. target: workspace|user. type: prompts|chatmodes|instructions|all')
256
- .option('-t, --type <type>', 'Specify type: prompts|chatmodes|instructions|all')
262
+ cli.command('install <target> [names...]', 'Install items into workspace or user profile. target: workspace|user. type: prompts|chatmodes|agents|instructions|skills|all')
263
+ .option('-t, --type <type>', 'Specify type: prompts|chatmodes|agents|instructions|skills|all')
257
264
  // Note: --refresh is intentionally not a per-command option for install; use only with list/search
258
265
  .option('--referesh', "Alias for --refresh (typo alias)")
259
266
  .option('--dry-run', 'Show what would be installed without writing files')
260
267
  .action(async (target, names, options) => {
261
268
  // names may be undefined or an array. Support legacy positional type in case
262
269
  // the user still passed it as the first name (e.g. `install workspace prompts p1`).
263
- const TYPES = ['prompts','chatmodes','instructions','all'];
270
+ const TYPES = ['prompts','chatmodes','agents','instructions','skills','all'];
264
271
  let type = options.type;
265
272
  // Normalize names to an array. Some CLI parsers may provide a single
266
273
  // name as a string instead of a one-element array. Preserve values.
@@ -1,4 +1,4 @@
1
- const { fetchIndex, diskPaths } = require('../fetcher');
1
+ const { fetchIndex, diskPaths, cacheExists } = require('../fetcher');
2
2
  const cache = require('../cache');
3
3
  const fs = require('fs-extra');
4
4
 
@@ -18,14 +18,20 @@ function formatItems(index, filter) {
18
18
  if (!filter || filter === 'all' || filter === 'chatmodes') {
19
19
  if (index.chatmodes) out.push(...index.chatmodes.map(c => ({ type: 'chatmode', id: displayId(c), name: c.name })));
20
20
  }
21
+ if (!filter || filter === 'all' || filter === 'agents') {
22
+ if (index.agents) out.push(...index.agents.map(a => ({ type: 'agent', id: displayId(a), name: a.name })));
23
+ }
21
24
  if (!filter || filter === 'all' || filter === 'instructions') {
22
25
  if (index.instructions) out.push(...index.instructions.map(i => ({ type: 'instruction', id: displayId(i), name: i.name })));
23
26
  }
27
+ if (!filter || filter === 'all' || filter === 'skills') {
28
+ if (index.skills) out.push(...index.skills.map(s => ({ type: 'skill', id: displayId(s), name: s.name })));
29
+ }
24
30
  return out;
25
31
  }
26
32
 
27
33
  function listCommand(cli) {
28
- cli.command('list [type]', 'List available items (prompts, chatmodes, instructions, all)')
34
+ cli.command('list [type]', 'List available items (prompts, chatmodes, agents, instructions, skills, all)')
29
35
  .option('-r, --refresh', 'Clear caches and force refresh from remote')
30
36
  .option('-j, --json', 'Emit machine-readable JSON output')
31
37
  .action(async (type, options) => {
@@ -52,6 +58,13 @@ function listCommand(cli) {
52
58
  let index = cache.get(key);
53
59
  if (!index) {
54
60
  try {
61
+ // Check if disk cache exists to determine if this is a fresh fetch
62
+ const diskCacheExists = await cacheExists();
63
+
64
+ if (!diskCacheExists && !options.refresh) {
65
+ console.log('Updating cache for the first time... (use -r to force refresh)');
66
+ }
67
+
55
68
  index = await fetchIndex();
56
69
  cache.set(key, index);
57
70
  } catch (err) {
@@ -1,10 +1,10 @@
1
- const { fetchIndex, diskPaths } = require('../fetcher');
1
+ const { fetchIndex, diskPaths, cacheExists } = require('../fetcher');
2
2
  const cache = require('../cache');
3
3
  const fs = require('fs-extra');
4
4
  const { formatListLines, visibleLines } = require('./list');
5
5
 
6
6
  function searchCommand(cli) {
7
- cli.command('search <query>', 'Search prompts, chatmodes, instructions')
7
+ cli.command('search <query>', 'Search prompts, chatmodes, agents, instructions, skills')
8
8
  .option('-r, --refresh', 'Clear caches and force refresh from remote')
9
9
  .option('-j, --json', 'Emit machine-readable JSON output')
10
10
  .action(async (query, options) => {
@@ -27,6 +27,13 @@ function searchCommand(cli) {
27
27
  let index = cache.get(key);
28
28
  if (!index) {
29
29
  try {
30
+ // Check if disk cache exists to determine if this is a fresh fetch
31
+ const diskCacheExists = await cacheExists();
32
+
33
+ if (!diskCacheExists && !options.refresh) {
34
+ console.log('Updating cache for the first time... (use -r to force refresh)');
35
+ }
36
+
30
37
  index = await fetchIndex();
31
38
  cache.set(key, index);
32
39
  } catch (err) {
@@ -37,7 +44,7 @@ function searchCommand(cli) {
37
44
  }
38
45
  const q = query.toLowerCase();
39
46
  const results = [];
40
- ['prompts','chatmodes','instructions'].forEach(cat => {
47
+ ['prompts','chatmodes','agents','instructions','skills'].forEach(cat => {
41
48
  const arr = index[cat] || [];
42
49
  arr.forEach(item => {
43
50
  const name = (item.name || item.id || '').toLowerCase();
@@ -66,7 +73,7 @@ function searchCommand(cli) {
66
73
  function searchIndex(index, query) {
67
74
  const q = query.toLowerCase();
68
75
  const results = [];
69
- ['prompts','chatmodes','instructions'].forEach(cat => {
76
+ ['prompts','chatmodes','agents','instructions','skills'].forEach(cat => {
70
77
  const arr = index[cat] || [];
71
78
  arr.forEach(item => {
72
79
  const name = (item.name || item.id || '').toLowerCase();
@@ -1,7 +1,4 @@
1
1
  const prompts = require('prompts');
2
- const cache = require('../cache');
3
- const { fetchIndex, diskPaths } = require('../fetcher');
4
- const fs = require('fs-extra');
5
2
  const { removeFiles } = require('../installer');
6
3
 
7
4
  function uninstallCommand(cli) {
@@ -11,22 +8,6 @@ function uninstallCommand(cli) {
11
8
  .action(async (target, type, names, options) => {
12
9
  const workspaceDir = process.cwd();
13
10
  if (options && options.verbose) console.log('verbose: starting uninstall', { target, type, names });
14
- const key = 'index';
15
- if (options && options.refresh) {
16
- if (options && options.verbose) console.log('verbose: refresh requested - clearing caches');
17
- try { cache.del(key); } catch (e) { if (options && options.verbose) console.log('verbose: failed to clear in-memory cache', e.message); }
18
- try { const { DISK_CACHE_FILE } = diskPaths(); if (fs.existsSync(DISK_CACHE_FILE)) fs.removeSync(DISK_CACHE_FILE); } catch (e) { if (options && options.verbose) console.log('verbose: failed to remove disk cache', e.message); }
19
- }
20
- let index = cache.get(key);
21
- if (!index) {
22
- try {
23
- index = await fetchIndex();
24
- cache.set(key, index);
25
- } catch (err) {
26
- console.error('Error fetching index:', err.message);
27
- return process.exitCode = 2;
28
- }
29
- }
30
11
 
31
12
  const toRemove = names && names.length > 0 ? names : [];
32
13
  if (toRemove.length === 0) {
package/src/fetcher.js CHANGED
@@ -1,8 +1,18 @@
1
1
  const axios = require('axios');
2
2
  const path = require('path');
3
3
  const fs = require('fs-extra');
4
+ const os = require('os');
4
5
  const cache = require('./cache');
5
6
 
7
+ // Helper to consistently decide where to store user-level data. In
8
+ // development and test environments prefer process.cwd() for predictable
9
+ // local workflows; otherwise use the user's homedir under ~/.acp.
10
+ function getBaseDir() {
11
+ return (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test')
12
+ ? process.cwd()
13
+ : path.join(os.homedir(), '.acp');
14
+ }
15
+
6
16
  // Default single upstream repo for backwards compatibility
7
17
  const DEFAULT_REPOS = [
8
18
  {
@@ -13,7 +23,11 @@ const DEFAULT_REPOS = [
13
23
  ];
14
24
 
15
25
  function diskPaths() {
16
- const DISK_CACHE_DIR = path.join(process.cwd(), '.acp-cache');
26
+ // In development and tests keep the old behavior (cwd) to avoid surprising
27
+ // local workflows and test expectations. Otherwise store cache under the
28
+ // user's home directory in ~/.acp/cache/index.json
29
+ const baseDir = getBaseDir();
30
+ const DISK_CACHE_DIR = path.join(baseDir, 'cache');
17
31
  const DISK_CACHE_FILE = path.join(DISK_CACHE_DIR, 'index.json');
18
32
  return { DISK_CACHE_DIR, DISK_CACHE_FILE };
19
33
  }
@@ -70,7 +84,7 @@ async function fetchIndex() {
70
84
  }
71
85
  }
72
86
 
73
- // Build repos list from env or default
87
+ // Build repos list from env, file, or default
74
88
  let repos = DEFAULT_REPOS;
75
89
  if (process.env.ACP_REPOS_JSON) {
76
90
  try {
@@ -79,6 +93,18 @@ async function fetchIndex() {
79
93
  } catch (e) {
80
94
  // ignore malformed env var and fall back to defaults
81
95
  }
96
+ } else {
97
+ // Try reading acp-repos.json from the user acp dir. Respect NODE_ENV handling
98
+ try {
99
+ const baseDir = getBaseDir();
100
+ const repoFile = path.join(baseDir, 'acp-repos.json');
101
+ if (await fs.pathExists(repoFile)) {
102
+ const fileContents = await fs.readJson(repoFile);
103
+ if (Array.isArray(fileContents) && fileContents.length > 0) repos = fileContents;
104
+ }
105
+ } catch (e) {
106
+ // ignore file read/parse errors and fall back to defaults
107
+ }
82
108
  }
83
109
 
84
110
  // Helper to parse frontmatter title from file content
@@ -98,7 +124,7 @@ async function fetchIndex() {
98
124
 
99
125
  // fetch each repo's tree and build combined index
100
126
  try {
101
- const combined = { prompts: [], chatmodes: [], instructions: [] };
127
+ const combined = { prompts: [], chatmodes: [], agents: [], instructions: [], skills: [] };
102
128
  for (const repo of repos) {
103
129
  if (!repo || !repo.treeUrl) continue;
104
130
  let res = null;
@@ -112,9 +138,9 @@ async function fetchIndex() {
112
138
  continue;
113
139
  }
114
140
  if (!res || res.status !== 200 || !res.data) continue;
115
- // If the remote returned a pre-built index object (prompts/chatmodes/instructions),
141
+ // If the remote returned a pre-built index object (prompts/chatmodes/agents/instructions/skills),
116
142
  // honor it and return immediately (backwards compatibility + tests).
117
- if (!Array.isArray(res.data.tree) && (res.data.prompts || res.data.chatmodes || res.data.instructions)) {
143
+ if (!Array.isArray(res.data.tree) && (res.data.prompts || res.data.chatmodes || res.data.agents || res.data.instructions || res.data.skills)) {
118
144
  // Remote returned a pre-built index object. Return it unmodified for
119
145
  // backwards compatibility and tests which expect the exact shape.
120
146
  idx = res.data;
@@ -129,7 +155,7 @@ async function fetchIndex() {
129
155
  const matches = tree.filter(t => t.path.startsWith(`${prefix}/`));
130
156
  const parts = await Promise.all(matches.map(async t => {
131
157
  const file = path.basename(t.path);
132
- const base = file.replace(/\.prompt\.md$|\.chatmode\.md$|\.instructions?\.md$/i, '').replace(/\.md$/i, '');
158
+ const base = file.replace(/\.prompt\.md$|\.chatmode\.md$|\.agent\.md$|\.instructions?\.md$/i, '').replace(/\.md$/i, '');
133
159
  const id = base;
134
160
  let name = base.replace(/[-_]+/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
135
161
  const rawBase = (repo.rawBase || repo.url || '').replace(/\/$/, '');
@@ -150,12 +176,14 @@ async function fetchIndex() {
150
176
 
151
177
  combined.prompts.push(...(await makeEntriesForRepo('prompts')));
152
178
  combined.chatmodes.push(...(await makeEntriesForRepo('chatmodes')));
179
+ combined.agents.push(...(await makeEntriesForRepo('agents')));
153
180
  combined.instructions.push(...(await makeEntriesForRepo('instructions')));
181
+ combined.skills.push(...(await makeEntriesForRepo('skills')));
154
182
  }
155
183
 
156
184
  // detect id conflicts across repos
157
185
  const idCounts = new Map();
158
- for (const cat of ['prompts','chatmodes','instructions']) {
186
+ for (const cat of ['prompts','chatmodes','agents','instructions','skills']) {
159
187
  for (const it of combined[cat]) {
160
188
  const k = it.id || it.name || '';
161
189
  if (!k) continue;
@@ -169,7 +197,7 @@ async function fetchIndex() {
169
197
 
170
198
  // If combined result is empty (no items found for any repo) and we have
171
199
  // a stale disk cache, prefer returning the disk payload as a fallback.
172
- const totalItems = combined.prompts.length + combined.chatmodes.length + combined.instructions.length;
200
+ const totalItems = combined.prompts.length + combined.chatmodes.length + combined.agents.length + combined.instructions.length + combined.skills.length;
173
201
  if (totalItems === 0 && disk && disk.payload) return disk.payload;
174
202
 
175
203
  idx = combined;
@@ -187,4 +215,13 @@ async function fetchIndex() {
187
215
  }
188
216
  }
189
217
 
190
- module.exports = { fetchIndex, diskPaths };
218
+ async function cacheExists() {
219
+ try {
220
+ const { DISK_CACHE_FILE } = diskPaths();
221
+ return await fs.pathExists(DISK_CACHE_FILE);
222
+ } catch (e) {
223
+ return false;
224
+ }
225
+ }
226
+
227
+ module.exports = { fetchIndex, diskPaths, cacheExists };
package/src/installer.js CHANGED
@@ -25,11 +25,13 @@ function getVsCodeUserDir() {
25
25
  }
26
26
 
27
27
  async function installFiles({ items, type, target, workspaceDir }) {
28
- // type: prompts|chatmodes|instructions
28
+ // type: prompts|chatmodes|agents|instructions|skills
29
29
  // Helper to derive filename and extension
30
30
  const extForType = t => {
31
31
  if (t === 'chatmodes') return '.chatmode.md';
32
+ if (t === 'agents') return '.agent.md';
32
33
  if (t === 'instructions' || t === 'instruction') return '.instructions.md';
34
+ if (t === 'skills') return '.md'; // skills typically use plain .md
33
35
  return '.prompt.md';
34
36
  };
35
37