@ekroon/opencode-copilot-instructions 0.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.
- package/README.md +103 -0
- package/dist/frontmatter.d.ts +14 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +51 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +22 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +64 -0
- package/dist/loader.js.map +1 -0
- package/dist/matcher.d.ts +18 -0
- package/dist/matcher.d.ts.map +1 -0
- package/dist/matcher.js +36 -0
- package/dist/matcher.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @ekroon/opencode-copilot-instructions
|
|
2
|
+
|
|
3
|
+
An OpenCode plugin that automatically loads GitHub Copilot custom instruction files and injects them into OpenCode's context.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From npm (recommended)
|
|
8
|
+
|
|
9
|
+
Add the plugin to your `opencode.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"plugin": ["@ekroon/opencode-copilot-instructions"]
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
OpenCode will automatically install the package from npm on startup.
|
|
18
|
+
|
|
19
|
+
### From local files
|
|
20
|
+
|
|
21
|
+
For development or customization, you can use the local plugin approach:
|
|
22
|
+
|
|
23
|
+
1. Create `.opencode/plugin/` directory in your project
|
|
24
|
+
2. Bundle the plugin: `bun build ./src/index.ts --outfile=.opencode/plugin/copilot.js --target=node --external:@opencode-ai/plugin`
|
|
25
|
+
3. Create `.opencode/package.json` with dependencies:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"front-matter": "^4.0.2",
|
|
31
|
+
"picomatch": "^4.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
4. Run `bun install` in the `.opencode/` directory
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
This plugin supports two types of GitHub Copilot instruction files:
|
|
41
|
+
|
|
42
|
+
### Repository-wide Instructions
|
|
43
|
+
|
|
44
|
+
Create `.github/copilot-instructions.md` in your repository root. These instructions apply to all files and are included in every session.
|
|
45
|
+
|
|
46
|
+
```markdown
|
|
47
|
+
# Project Guidelines
|
|
48
|
+
|
|
49
|
+
- Use TypeScript strict mode
|
|
50
|
+
- Prefer functional programming patterns
|
|
51
|
+
- Write comprehensive tests for all new code
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Path-specific Instructions
|
|
55
|
+
|
|
56
|
+
Create files matching `.github/instructions/*.instructions.md`. These instructions only apply when working with files that match the specified glob patterns.
|
|
57
|
+
|
|
58
|
+
Each file requires YAML frontmatter with an `applyTo` field:
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
---
|
|
62
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
When writing TypeScript code:
|
|
66
|
+
|
|
67
|
+
- Always use explicit return types
|
|
68
|
+
- Prefer interfaces over type aliases for object shapes
|
|
69
|
+
- Use branded types for IDs
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `applyTo` field accepts a comma-separated list of glob patterns.
|
|
73
|
+
|
|
74
|
+
## How it Works
|
|
75
|
+
|
|
76
|
+
### Loading
|
|
77
|
+
|
|
78
|
+
Instructions are loaded once when the plugin initializes (at OpenCode startup). To reload instructions after changes, restart OpenCode.
|
|
79
|
+
|
|
80
|
+
### Injection
|
|
81
|
+
|
|
82
|
+
- **Repository-wide instructions**: Injected via the `experimental.session.compacting` hook, ensuring they persist across session compaction.
|
|
83
|
+
|
|
84
|
+
- **Path-specific instructions**: Injected via the `tool.execute.before` hook when file operations (read, edit, write) target files matching the `applyTo` patterns.
|
|
85
|
+
|
|
86
|
+
## Supported Glob Patterns
|
|
87
|
+
|
|
88
|
+
The plugin uses [picomatch](https://github.com/micromatch/picomatch) for pattern matching. Supported patterns include:
|
|
89
|
+
|
|
90
|
+
| Pattern | Description |
|
|
91
|
+
|---------|-------------|
|
|
92
|
+
| `*` | All files in current directory |
|
|
93
|
+
| `**` or `**/*` | All files in all directories |
|
|
94
|
+
| `*.py` | All `.py` files in current directory |
|
|
95
|
+
| `**/*.py` | All `.py` files recursively |
|
|
96
|
+
| `src/**/*.py` | All `.py` files in `src` directory recursively |
|
|
97
|
+
| `**/*.{ts,tsx}` | All `.ts` and `.tsx` files recursively |
|
|
98
|
+
|
|
99
|
+
Multiple patterns can be combined with commas: `**/*.ts,**/*.tsx,**/*.js`
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface Frontmatter {
|
|
2
|
+
applyTo?: string | string[];
|
|
3
|
+
excludeAgent?: "code-review" | "coding-agent";
|
|
4
|
+
}
|
|
5
|
+
export interface ParsedContent {
|
|
6
|
+
frontmatter: Frontmatter;
|
|
7
|
+
body: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parses YAML frontmatter from markdown-style content.
|
|
11
|
+
* Frontmatter must be delimited by --- at the start of the file.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseFrontmatter(content: string): ParsedContent;
|
|
14
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IAC3B,YAAY,CAAC,EAAE,aAAa,GAAG,cAAc,CAAA;CAC9C;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,WAAW,CAAA;IACxB,IAAI,EAAE,MAAM,CAAA;CACb;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CA8C/D"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as frontMatter from 'front-matter';
|
|
2
|
+
// Handle both ESM and CommonJS module formats
|
|
3
|
+
const fm = frontMatter.default ?? frontMatter;
|
|
4
|
+
/**
|
|
5
|
+
* Parses YAML frontmatter from markdown-style content.
|
|
6
|
+
* Frontmatter must be delimited by --- at the start of the file.
|
|
7
|
+
*/
|
|
8
|
+
export function parseFrontmatter(content) {
|
|
9
|
+
// Normalize line endings to \n for consistent parsing
|
|
10
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
11
|
+
// Check if content has frontmatter
|
|
12
|
+
if (!fm.test(normalized)) {
|
|
13
|
+
return {
|
|
14
|
+
frontmatter: {},
|
|
15
|
+
body: content
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const parsed = fm(normalized);
|
|
20
|
+
const attrs = parsed.attributes;
|
|
21
|
+
// Extract only known properties with validation
|
|
22
|
+
const result = {};
|
|
23
|
+
if (attrs.applyTo !== undefined) {
|
|
24
|
+
// Accept string or array of strings
|
|
25
|
+
if (typeof attrs.applyTo === 'string') {
|
|
26
|
+
result.applyTo = attrs.applyTo;
|
|
27
|
+
}
|
|
28
|
+
else if (Array.isArray(attrs.applyTo)) {
|
|
29
|
+
result.applyTo = attrs.applyTo.filter((item) => typeof item === 'string');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (attrs.excludeAgent !== undefined) {
|
|
33
|
+
const agent = attrs.excludeAgent;
|
|
34
|
+
if (agent === 'code-review' || agent === 'coding-agent') {
|
|
35
|
+
result.excludeAgent = agent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
frontmatter: result,
|
|
40
|
+
body: parsed.body
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Malformed YAML - return original content as body
|
|
45
|
+
return {
|
|
46
|
+
frontmatter: {},
|
|
47
|
+
body: content
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=frontmatter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,cAAc,CAAA;AAE3C,8CAA8C;AAC9C,MAAM,EAAE,GAAI,WAAmB,CAAC,OAAO,IAAI,WAAW,CAAA;AAiBtD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,sDAAsD;IACtD,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAEjD,mCAAmC;IACnC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACzB,OAAO;YACL,WAAW,EAAE,EAAE;YACf,IAAI,EAAE,OAAO;SACd,CAAA;IACH,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,EAAE,CAAC,UAAU,CAAiD,CAAA;QAC7E,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAA;QAE/B,gDAAgD;QAChD,MAAM,MAAM,GAAgB,EAAE,CAAA;QAE9B,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAChC,oCAAoC;YACpC,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBACtC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;YAChC,CAAC;iBAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBACxC,MAAM,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,IAAa,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAA;YACpG,CAAC;QACH,CAAC;QAED,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACrC,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY,CAAA;YAChC,IAAI,KAAK,KAAK,aAAa,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;gBACxD,MAAM,CAAC,YAAY,GAAG,KAAK,CAAA;YAC7B,CAAC;QACH,CAAC;QAED,OAAO;YACL,WAAW,EAAE,MAAM;YACnB,IAAI,EAAE,MAAM,CAAC,IAAI;SAClB,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,mDAAmD;QACnD,OAAO;YACL,WAAW,EAAE,EAAE;YACf,IAAI,EAAE,OAAO;SACd,CAAA;IACH,CAAC;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AAwBjD,eAAO,MAAM,yBAAyB,EAAE,MAwGvC,CAAA;AAGD,eAAe,yBAAyB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { loadRepoInstructions, loadPathInstructions } from './loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* Convert an absolute path to a relative path from the given directory.
|
|
5
|
+
* If the path is already relative, returns it as-is.
|
|
6
|
+
* Note: This is intentionally NOT exported to avoid OpenCode treating it as a plugin
|
|
7
|
+
*/
|
|
8
|
+
function getRelativePath(directory, filePath) {
|
|
9
|
+
// Normalize directory path (remove trailing slash)
|
|
10
|
+
const normalizedDir = directory.endsWith('/') ? directory.slice(0, -1) : directory;
|
|
11
|
+
// If path is already relative (doesn't start with /), return as-is
|
|
12
|
+
if (!path.isAbsolute(filePath)) {
|
|
13
|
+
return filePath;
|
|
14
|
+
}
|
|
15
|
+
// Use path.relative to compute relative path
|
|
16
|
+
return path.relative(normalizedDir, filePath);
|
|
17
|
+
}
|
|
18
|
+
// Tools that work with file paths
|
|
19
|
+
const FILE_TOOLS = new Set(['read', 'edit', 'write']);
|
|
20
|
+
export const CopilotInstructionsPlugin = async (ctx) => {
|
|
21
|
+
const { directory, client } = ctx;
|
|
22
|
+
// Validate directory is provided and is a string
|
|
23
|
+
if (!directory || typeof directory !== 'string') {
|
|
24
|
+
console.error('[copilot-instructions] Invalid directory:', directory, 'ctx:', Object.keys(ctx));
|
|
25
|
+
throw new Error(`Plugin requires a valid directory string, got: ${typeof directory}`);
|
|
26
|
+
}
|
|
27
|
+
// Store directory in a local const to ensure closure captures it properly
|
|
28
|
+
const projectDir = directory;
|
|
29
|
+
// Load instructions at startup
|
|
30
|
+
const repoInstructions = loadRepoInstructions(projectDir);
|
|
31
|
+
const pathInstructions = loadPathInstructions(projectDir);
|
|
32
|
+
// Helper to log messages
|
|
33
|
+
const log = (message) => {
|
|
34
|
+
client.app.log({
|
|
35
|
+
body: {
|
|
36
|
+
service: 'copilot-instructions',
|
|
37
|
+
level: 'info',
|
|
38
|
+
message
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
// Log what was loaded
|
|
43
|
+
if (repoInstructions) {
|
|
44
|
+
log('Loaded repo instructions from .github/copilot-instructions.md');
|
|
45
|
+
}
|
|
46
|
+
if (pathInstructions.length > 0) {
|
|
47
|
+
for (const instruction of pathInstructions) {
|
|
48
|
+
const filename = path.basename(instruction.file);
|
|
49
|
+
log(`Loaded path instructions from ${filename}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!repoInstructions && pathInstructions.length === 0) {
|
|
53
|
+
log('No Copilot instructions found');
|
|
54
|
+
}
|
|
55
|
+
// Track injected instructions per session
|
|
56
|
+
// Map<sessionID, Set<instructionFile>>
|
|
57
|
+
const injectedPerSession = new Map();
|
|
58
|
+
return {
|
|
59
|
+
'experimental.session.compacting': async (_input, output) => {
|
|
60
|
+
if (repoInstructions) {
|
|
61
|
+
output.context.push(`## Copilot Custom Instructions\n\n${repoInstructions}`);
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
'tool.execute.before': async (input, output) => {
|
|
65
|
+
// Only handle file tools
|
|
66
|
+
if (!FILE_TOOLS.has(input.tool)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Get file path from args
|
|
70
|
+
const filePath = output.args?.filePath;
|
|
71
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Convert to relative path for matching
|
|
75
|
+
const relativePath = getRelativePath(projectDir, filePath);
|
|
76
|
+
// Find matching instructions that haven't been injected yet
|
|
77
|
+
const sessionInjected = injectedPerSession.get(input.sessionID) ?? new Set();
|
|
78
|
+
injectedPerSession.set(input.sessionID, sessionInjected);
|
|
79
|
+
const matchingInstructions = [];
|
|
80
|
+
for (const instruction of pathInstructions) {
|
|
81
|
+
// Skip if already injected in this session
|
|
82
|
+
if (sessionInjected.has(instruction.file)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
// Check if file matches this instruction's patterns
|
|
86
|
+
if (instruction.matcher(relativePath)) {
|
|
87
|
+
matchingInstructions.push(instruction);
|
|
88
|
+
sessionInjected.add(instruction.file);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Inject matching instructions
|
|
92
|
+
if (matchingInstructions.length > 0) {
|
|
93
|
+
const instructionText = matchingInstructions
|
|
94
|
+
.map(i => i.content)
|
|
95
|
+
.join('\n\n');
|
|
96
|
+
// Prepend to existing toolMessage if any
|
|
97
|
+
const existingMessage = output.toolMessage;
|
|
98
|
+
if (existingMessage) {
|
|
99
|
+
output.toolMessage = `${instructionText}\n\n${existingMessage}`;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
output.toolMessage = instructionText;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
// Default export for easier loading
|
|
109
|
+
export default CopilotInstructionsPlugin;
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Matcher } from './matcher.js';
|
|
2
|
+
export interface PathInstruction {
|
|
3
|
+
file: string;
|
|
4
|
+
applyTo: string[];
|
|
5
|
+
content: string;
|
|
6
|
+
matcher: Matcher;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Load repository-wide Copilot instructions from .github/copilot-instructions.md
|
|
10
|
+
*
|
|
11
|
+
* @param directory - The root directory to search in
|
|
12
|
+
* @returns The file content as a string if found, null otherwise
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadRepoInstructions(directory: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Load path-specific Copilot instructions from .github/instructions/*.instructions.md
|
|
17
|
+
*
|
|
18
|
+
* @param directory - The root directory to search in
|
|
19
|
+
* @returns Array of PathInstruction objects for each valid instruction file
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadPathInstructions(directory: string): PathInstruction[];
|
|
22
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,OAAO,EAAE,MAAM,cAAc,CAAA;AAE7E,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQrE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,EAAE,CA4CzE"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { parseFrontmatter } from './frontmatter.js';
|
|
4
|
+
import { createMatcher, normalizePatterns } from './matcher.js';
|
|
5
|
+
/**
|
|
6
|
+
* Load repository-wide Copilot instructions from .github/copilot-instructions.md
|
|
7
|
+
*
|
|
8
|
+
* @param directory - The root directory to search in
|
|
9
|
+
* @returns The file content as a string if found, null otherwise
|
|
10
|
+
*/
|
|
11
|
+
export function loadRepoInstructions(directory) {
|
|
12
|
+
const filePath = path.join(directory, '.github', 'copilot-instructions.md');
|
|
13
|
+
try {
|
|
14
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load path-specific Copilot instructions from .github/instructions/*.instructions.md
|
|
22
|
+
*
|
|
23
|
+
* @param directory - The root directory to search in
|
|
24
|
+
* @returns Array of PathInstruction objects for each valid instruction file
|
|
25
|
+
*/
|
|
26
|
+
export function loadPathInstructions(directory) {
|
|
27
|
+
const instructionsDir = path.join(directory, '.github', 'instructions');
|
|
28
|
+
let files;
|
|
29
|
+
try {
|
|
30
|
+
files = fs.readdirSync(instructionsDir);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const result = [];
|
|
36
|
+
for (const filename of files) {
|
|
37
|
+
// Only process *.instructions.md files
|
|
38
|
+
if (!filename.endsWith('.instructions.md')) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const filePath = path.join(instructionsDir, filename);
|
|
42
|
+
let content;
|
|
43
|
+
try {
|
|
44
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const parsed = parseFrontmatter(content);
|
|
50
|
+
const patterns = normalizePatterns(parsed.frontmatter.applyTo);
|
|
51
|
+
// Skip files without applyTo patterns
|
|
52
|
+
if (patterns.length === 0) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
result.push({
|
|
56
|
+
file: filePath,
|
|
57
|
+
applyTo: patterns,
|
|
58
|
+
content: parsed.body,
|
|
59
|
+
matcher: createMatcher(patterns)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAA;AAC7B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,iBAAiB,EAAgB,MAAM,cAAc,CAAA;AAS7E;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,yBAAyB,CAAC,CAAA;IAE3E,IAAI,CAAC;QACH,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,CAAC,CAAA;IAEvE,IAAI,KAAe,CAAA;IACnB,IAAI,CAAC;QACH,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,MAAM,GAAsB,EAAE,CAAA;IAEpC,KAAK,MAAM,QAAQ,IAAI,KAAK,EAAE,CAAC;QAC7B,uCAAuC;QACvC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,CAAC;YAC3C,SAAQ;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAC,CAAA;QAErD,IAAI,OAAe,CAAA;QACnB,IAAI,CAAC;YACH,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAC9C,CAAC;QAAC,MAAM,CAAC;YACP,SAAQ;QACV,CAAC;QAED,MAAM,MAAM,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAA;QACxC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAE9D,sCAAsC;QACtC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,SAAQ;QACV,CAAC;QAED,MAAM,CAAC,IAAI,CAAC;YACV,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,QAAQ;YACjB,OAAO,EAAE,MAAM,CAAC,IAAI;YACpB,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC;SACjC,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Matcher = (path: string) => boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Creates a matcher function that tests paths against glob patterns.
|
|
4
|
+
* Returns a function that returns true if the path matches any of the patterns.
|
|
5
|
+
*
|
|
6
|
+
* @param patterns - Array of glob patterns to match against
|
|
7
|
+
* @returns A function that tests if a path matches any pattern
|
|
8
|
+
*/
|
|
9
|
+
export declare function createMatcher(patterns: string[]): Matcher;
|
|
10
|
+
/**
|
|
11
|
+
* Normalizes the applyTo field from frontmatter into an array of patterns.
|
|
12
|
+
* Handles undefined, single strings, comma-separated strings, and arrays.
|
|
13
|
+
*
|
|
14
|
+
* @param applyTo - The applyTo value from frontmatter
|
|
15
|
+
* @returns Normalized array of patterns
|
|
16
|
+
*/
|
|
17
|
+
export declare function normalizePatterns(applyTo: string | string[] | undefined): string[];
|
|
18
|
+
//# sourceMappingURL=matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.d.ts","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;AAE/C;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAOzD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAclF"}
|
package/dist/matcher.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import picomatch from 'picomatch';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a matcher function that tests paths against glob patterns.
|
|
4
|
+
* Returns a function that returns true if the path matches any of the patterns.
|
|
5
|
+
*
|
|
6
|
+
* @param patterns - Array of glob patterns to match against
|
|
7
|
+
* @returns A function that tests if a path matches any pattern
|
|
8
|
+
*/
|
|
9
|
+
export function createMatcher(patterns) {
|
|
10
|
+
if (patterns.length === 0) {
|
|
11
|
+
return () => false;
|
|
12
|
+
}
|
|
13
|
+
const isMatch = picomatch(patterns);
|
|
14
|
+
return (path) => isMatch(path);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes the applyTo field from frontmatter into an array of patterns.
|
|
18
|
+
* Handles undefined, single strings, comma-separated strings, and arrays.
|
|
19
|
+
*
|
|
20
|
+
* @param applyTo - The applyTo value from frontmatter
|
|
21
|
+
* @returns Normalized array of patterns
|
|
22
|
+
*/
|
|
23
|
+
export function normalizePatterns(applyTo) {
|
|
24
|
+
if (applyTo === undefined) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(applyTo)) {
|
|
28
|
+
return applyTo;
|
|
29
|
+
}
|
|
30
|
+
// Handle string input - split by comma and trim whitespace
|
|
31
|
+
return applyTo
|
|
32
|
+
.split(',')
|
|
33
|
+
.map((pattern) => pattern.trim())
|
|
34
|
+
.filter((pattern) => pattern.length > 0);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"matcher.js","sourceRoot":"","sources":["../src/matcher.ts"],"names":[],"mappings":"AAAA,OAAO,SAAS,MAAM,WAAW,CAAA;AAIjC;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,QAAkB;IAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,GAAG,EAAE,CAAC,KAAK,CAAA;IACpB,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;IACnC,OAAO,CAAC,IAAY,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;AACxC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAsC;IACtE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,2DAA2D;IAC3D,OAAO,OAAO;SACX,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;SAChC,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAC5C,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekroon/opencode-copilot-instructions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"front-matter": "^4.0.2",
|
|
23
|
+
"picomatch": "^4.0.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@opencode-ai/plugin": "*"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "latest",
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"@types/picomatch": "^3.0.0",
|
|
32
|
+
"typescript": "^5.0.0",
|
|
33
|
+
"vitest": "^3.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|