@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 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"}
@@ -0,0 +1,4 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ export declare const CopilotInstructionsPlugin: Plugin;
3
+ export default CopilotInstructionsPlugin;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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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"}
@@ -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"}
@@ -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
+ }