opencode-snippets 1.4.2 → 1.4.3
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/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/src/arg-parser.d.ts +16 -0
- package/dist/src/arg-parser.d.ts.map +1 -0
- package/dist/src/arg-parser.js +94 -0
- package/dist/src/arg-parser.js.map +1 -0
- package/dist/src/commands.d.ts +30 -0
- package/dist/src/commands.d.ts.map +1 -0
- package/dist/src/commands.js +315 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/constants.d.ts +26 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +28 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/expander.d.ts +36 -0
- package/dist/src/expander.d.ts.map +1 -0
- package/dist/src/expander.js +187 -0
- package/dist/src/expander.js.map +1 -0
- package/dist/src/loader.d.ts +46 -0
- package/dist/src/loader.d.ts.map +1 -0
- package/dist/src/loader.js +223 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/logger.d.ts +15 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +107 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/notification.d.ts +11 -0
- package/dist/src/notification.d.ts.map +1 -0
- package/dist/src/notification.js +26 -0
- package/dist/src/notification.js.map +1 -0
- package/dist/src/shell.d.ts +18 -0
- package/dist/src/shell.d.ts.map +1 -0
- package/dist/src/shell.js +30 -0
- package/dist/src/shell.js.map +1 -0
- package/dist/src/types.d.ts +65 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +8 -5
- package/index.ts +0 -81
- package/src/arg-parser.test.ts +0 -177
- package/src/arg-parser.ts +0 -87
- package/src/commands.test.ts +0 -188
- package/src/commands.ts +0 -414
- package/src/constants.ts +0 -32
- package/src/expander.test.ts +0 -846
- package/src/expander.ts +0 -225
- package/src/loader.test.ts +0 -352
- package/src/loader.ts +0 -268
- package/src/logger.test.ts +0 -136
- package/src/logger.ts +0 -121
- package/src/notification.ts +0 -30
- package/src/shell.ts +0 -50
- package/src/types.ts +0 -71
package/src/expander.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
import { PATTERNS } from "./constants.js";
|
|
2
|
-
import { logger } from "./logger.js";
|
|
3
|
-
import type { ExpansionResult, ParsedSnippetContent, SnippetRegistry } from "./types.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Maximum number of times a snippet can be expanded to prevent infinite loops
|
|
7
|
-
*/
|
|
8
|
-
const MAX_EXPANSION_COUNT = 15;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Tag types for parsing
|
|
12
|
-
*/
|
|
13
|
-
type BlockType = "prepend" | "append";
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Parses snippet content to extract inline text and prepend/append blocks
|
|
17
|
-
*
|
|
18
|
-
* Uses a lenient stack-based parser:
|
|
19
|
-
* - Unclosed tags → treat rest of content as block
|
|
20
|
-
* - Nesting → log error, return null (skip expansion)
|
|
21
|
-
* - Multiple blocks → collected in document order
|
|
22
|
-
*
|
|
23
|
-
* @param content - The raw snippet content to parse
|
|
24
|
-
* @returns Parsed content with inline, prepend, and append parts, or null on error
|
|
25
|
-
*/
|
|
26
|
-
export function parseSnippetBlocks(content: string): ParsedSnippetContent | null {
|
|
27
|
-
const prepend: string[] = [];
|
|
28
|
-
const append: string[] = [];
|
|
29
|
-
let inline = "";
|
|
30
|
-
|
|
31
|
-
// Regex to find opening and closing tags
|
|
32
|
-
const tagPattern = /<(\/?)(?<tagName>prepend|append)>/gi;
|
|
33
|
-
let lastIndex = 0;
|
|
34
|
-
let currentBlock: { type: BlockType; startIndex: number; contentStart: number } | null = null;
|
|
35
|
-
|
|
36
|
-
let match = tagPattern.exec(content);
|
|
37
|
-
while (match !== null) {
|
|
38
|
-
const isClosing = match[1] === "/";
|
|
39
|
-
const tagName = match.groups?.tagName?.toLowerCase() as BlockType;
|
|
40
|
-
const tagStart = match.index;
|
|
41
|
-
const tagEnd = tagStart + match[0].length;
|
|
42
|
-
|
|
43
|
-
if (isClosing) {
|
|
44
|
-
// Closing tag
|
|
45
|
-
if (currentBlock === null) {
|
|
46
|
-
// Closing tag without opening - ignore it, treat as inline content
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (currentBlock.type !== tagName) {
|
|
50
|
-
// Mismatched closing tag - this is a nesting error
|
|
51
|
-
logger.warn(
|
|
52
|
-
`Mismatched closing tag: expected </${currentBlock.type}>, found </${tagName}>`,
|
|
53
|
-
);
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
// Extract block content
|
|
57
|
-
const blockContent = content.slice(currentBlock.contentStart, tagStart).trim();
|
|
58
|
-
if (blockContent) {
|
|
59
|
-
if (currentBlock.type === "prepend") {
|
|
60
|
-
prepend.push(blockContent);
|
|
61
|
-
} else {
|
|
62
|
-
append.push(blockContent);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
lastIndex = tagEnd;
|
|
66
|
-
currentBlock = null;
|
|
67
|
-
} else {
|
|
68
|
-
// Opening tag
|
|
69
|
-
if (currentBlock !== null) {
|
|
70
|
-
// Nested opening tag - error
|
|
71
|
-
logger.warn(`Nested tags not allowed: found <${tagName}> inside <${currentBlock.type}>`);
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
// Add any inline content before this tag
|
|
75
|
-
const inlinePart = content.slice(lastIndex, tagStart);
|
|
76
|
-
inline += inlinePart;
|
|
77
|
-
currentBlock = { type: tagName, startIndex: tagStart, contentStart: tagEnd };
|
|
78
|
-
}
|
|
79
|
-
match = tagPattern.exec(content);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Handle unclosed tag (lenient: treat rest as block content)
|
|
83
|
-
if (currentBlock !== null) {
|
|
84
|
-
const blockContent = content.slice(currentBlock.contentStart).trim();
|
|
85
|
-
if (blockContent) {
|
|
86
|
-
if (currentBlock.type === "prepend") {
|
|
87
|
-
prepend.push(blockContent);
|
|
88
|
-
} else {
|
|
89
|
-
append.push(blockContent);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
} else {
|
|
93
|
-
// Add any remaining inline content
|
|
94
|
-
inline += content.slice(lastIndex);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
inline: inline.trim(),
|
|
99
|
-
prepend,
|
|
100
|
-
append,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Expands hashtags in text recursively with loop detection
|
|
106
|
-
*
|
|
107
|
-
* Returns an ExpansionResult containing the inline-expanded text plus
|
|
108
|
-
* collected prepend/append blocks from all expanded snippets.
|
|
109
|
-
*
|
|
110
|
-
* @param text - The text containing hashtags to expand
|
|
111
|
-
* @param registry - The snippet registry to look up hashtags
|
|
112
|
-
* @param expansionCounts - Map tracking how many times each snippet has been expanded
|
|
113
|
-
* @returns ExpansionResult with text and collected blocks
|
|
114
|
-
*/
|
|
115
|
-
export function expandHashtags(
|
|
116
|
-
text: string,
|
|
117
|
-
registry: SnippetRegistry,
|
|
118
|
-
expansionCounts = new Map<string, number>(),
|
|
119
|
-
): ExpansionResult {
|
|
120
|
-
const collectedPrepend: string[] = [];
|
|
121
|
-
const collectedAppend: string[] = [];
|
|
122
|
-
|
|
123
|
-
let expanded = text;
|
|
124
|
-
let hasChanges = true;
|
|
125
|
-
|
|
126
|
-
// Keep expanding until no more hashtags are found
|
|
127
|
-
while (hasChanges) {
|
|
128
|
-
const previous = expanded;
|
|
129
|
-
let loopDetected = false;
|
|
130
|
-
|
|
131
|
-
// Reset regex state (global flag requires this)
|
|
132
|
-
PATTERNS.HASHTAG.lastIndex = 0;
|
|
133
|
-
|
|
134
|
-
// We need to collect blocks during replacement, so we track them here
|
|
135
|
-
const roundPrepend: string[] = [];
|
|
136
|
-
const roundAppend: string[] = [];
|
|
137
|
-
|
|
138
|
-
expanded = expanded.replace(PATTERNS.HASHTAG, (match, name) => {
|
|
139
|
-
const key = name.toLowerCase();
|
|
140
|
-
|
|
141
|
-
const snippet = registry.get(key);
|
|
142
|
-
if (snippet === undefined) {
|
|
143
|
-
// Unknown snippet - leave as-is
|
|
144
|
-
return match;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Track expansion count to prevent infinite loops
|
|
148
|
-
const count = (expansionCounts.get(key) || 0) + 1;
|
|
149
|
-
if (count > MAX_EXPANSION_COUNT) {
|
|
150
|
-
// Loop detected! Leave the hashtag as-is and stop expanding
|
|
151
|
-
logger.warn(
|
|
152
|
-
`Loop detected: snippet '#${key}' expanded ${count} times (max: ${MAX_EXPANSION_COUNT})`,
|
|
153
|
-
);
|
|
154
|
-
loopDetected = true;
|
|
155
|
-
return match; // Leave as-is instead of error message
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
expansionCounts.set(key, count);
|
|
159
|
-
|
|
160
|
-
// Parse the snippet content for blocks
|
|
161
|
-
const parsed = parseSnippetBlocks(snippet.content);
|
|
162
|
-
if (parsed === null) {
|
|
163
|
-
// Parse error - leave hashtag unchanged
|
|
164
|
-
logger.warn(`Failed to parse snippet '${key}', leaving hashtag unchanged`);
|
|
165
|
-
return match;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Collect prepend/append blocks
|
|
169
|
-
roundPrepend.push(...parsed.prepend);
|
|
170
|
-
roundAppend.push(...parsed.append);
|
|
171
|
-
|
|
172
|
-
// Recursively expand any hashtags in the inline content
|
|
173
|
-
const nestedResult = expandHashtags(parsed.inline, registry, expansionCounts);
|
|
174
|
-
|
|
175
|
-
// Collect blocks from nested expansion
|
|
176
|
-
roundPrepend.push(...nestedResult.prepend);
|
|
177
|
-
roundAppend.push(...nestedResult.append);
|
|
178
|
-
|
|
179
|
-
return nestedResult.text;
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Add this round's blocks to collected blocks
|
|
183
|
-
collectedPrepend.push(...roundPrepend);
|
|
184
|
-
collectedAppend.push(...roundAppend);
|
|
185
|
-
|
|
186
|
-
// Only continue if the text actually changed AND no loop was detected
|
|
187
|
-
hasChanges = expanded !== previous && !loopDetected;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return {
|
|
191
|
-
text: expanded,
|
|
192
|
-
prepend: collectedPrepend,
|
|
193
|
-
append: collectedAppend,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Assembles the final message from an expansion result
|
|
199
|
-
*
|
|
200
|
-
* Joins: prepend blocks + inline text + append blocks
|
|
201
|
-
* with double newlines between non-empty sections.
|
|
202
|
-
*
|
|
203
|
-
* @param result - The expansion result to assemble
|
|
204
|
-
* @returns The final assembled message
|
|
205
|
-
*/
|
|
206
|
-
export function assembleMessage(result: ExpansionResult): string {
|
|
207
|
-
const parts: string[] = [];
|
|
208
|
-
|
|
209
|
-
// Add prepend blocks
|
|
210
|
-
if (result.prepend.length > 0) {
|
|
211
|
-
parts.push(result.prepend.join("\n\n"));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Add main text
|
|
215
|
-
if (result.text.trim()) {
|
|
216
|
-
parts.push(result.text);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Add append blocks
|
|
220
|
-
if (result.append.length > 0) {
|
|
221
|
-
parts.push(result.append.join("\n\n"));
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return parts.join("\n\n");
|
|
225
|
-
}
|
package/src/loader.test.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { loadSnippets } from "../src/loader.js";
|
|
4
|
-
|
|
5
|
-
describe("loadSnippets - Dual Path Support", () => {
|
|
6
|
-
let tempDir: string;
|
|
7
|
-
let globalSnippetDir: string;
|
|
8
|
-
let projectSnippetDir: string;
|
|
9
|
-
|
|
10
|
-
beforeEach(async () => {
|
|
11
|
-
// Create temporary directories for testing
|
|
12
|
-
tempDir = join(process.cwd(), ".test-temp");
|
|
13
|
-
globalSnippetDir = join(tempDir, "global-snippet");
|
|
14
|
-
projectSnippetDir = join(tempDir, "project", ".opencode", "snippet");
|
|
15
|
-
|
|
16
|
-
await mkdir(globalSnippetDir, { recursive: true });
|
|
17
|
-
await mkdir(projectSnippetDir, { recursive: true });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
// Clean up temporary directories
|
|
22
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
describe("Global snippets only", () => {
|
|
26
|
-
it("should load snippets with aliases", async () => {
|
|
27
|
-
await writeFile(
|
|
28
|
-
join(globalSnippetDir, "careful.md"),
|
|
29
|
-
`---
|
|
30
|
-
aliases: safe
|
|
31
|
-
---
|
|
32
|
-
Think step by step. Double-check your work.`,
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
36
|
-
|
|
37
|
-
expect(snippets.size).toBe(2);
|
|
38
|
-
expect(snippets.get("careful")?.content).toBe("Think step by step. Double-check your work.");
|
|
39
|
-
expect(snippets.get("safe")?.content).toBe("Think step by step. Double-check your work.");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("should load multiple snippets from global directory", async () => {
|
|
43
|
-
await writeFile(join(globalSnippetDir, "snippet1.md"), "Content of snippet 1");
|
|
44
|
-
await writeFile(join(globalSnippetDir, "snippet2.md"), "Content of snippet 2");
|
|
45
|
-
|
|
46
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
47
|
-
|
|
48
|
-
expect(snippets.size).toBe(2);
|
|
49
|
-
expect(snippets.get("snippet1")?.content).toBe("Content of snippet 1");
|
|
50
|
-
expect(snippets.get("snippet2")?.content).toBe("Content of snippet 2");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe("Project snippets only", () => {
|
|
55
|
-
it("should load snippets from project directory", async () => {
|
|
56
|
-
await writeFile(
|
|
57
|
-
join(projectSnippetDir, "project-specific.md"),
|
|
58
|
-
"This is a project-specific snippet",
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Load with project directory
|
|
62
|
-
const projectDir = join(tempDir, "project");
|
|
63
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
64
|
-
|
|
65
|
-
expect(snippets.size).toBe(1);
|
|
66
|
-
expect(snippets.get("project-specific")?.content).toBe("This is a project-specific snippet");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("should handle missing global directory when project exists", async () => {
|
|
70
|
-
await writeFile(join(projectSnippetDir, "team-rule.md"), "Team rule 1");
|
|
71
|
-
await writeFile(join(projectSnippetDir, "domain-knowledge.md"), "Domain knowledge");
|
|
72
|
-
|
|
73
|
-
const projectDir = join(tempDir, "project");
|
|
74
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
75
|
-
|
|
76
|
-
expect(snippets.size).toBe(2);
|
|
77
|
-
expect(snippets.get("team-rule")?.content).toBe("Team rule 1");
|
|
78
|
-
expect(snippets.get("domain-knowledge")?.content).toBe("Domain knowledge");
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
describe("Both global and project snippets", () => {
|
|
83
|
-
it("should merge global and project snippets", async () => {
|
|
84
|
-
// Create global snippet
|
|
85
|
-
await writeFile(join(globalSnippetDir, "global.md"), "Global snippet content");
|
|
86
|
-
|
|
87
|
-
// Create project snippet
|
|
88
|
-
await writeFile(join(projectSnippetDir, "project.md"), "Project snippet content");
|
|
89
|
-
|
|
90
|
-
const projectDir = join(tempDir, "project");
|
|
91
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
92
|
-
|
|
93
|
-
expect(snippets.size).toBe(2);
|
|
94
|
-
expect(snippets.get("global")?.content).toBe("Global snippet content");
|
|
95
|
-
expect(snippets.get("project")?.content).toBe("Project snippet content");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("should allow project snippets to override global snippets", async () => {
|
|
99
|
-
// Create global snippet
|
|
100
|
-
await writeFile(join(globalSnippetDir, "careful.md"), "Global careful content");
|
|
101
|
-
|
|
102
|
-
// Create project snippet with same name
|
|
103
|
-
await writeFile(join(projectSnippetDir, "careful.md"), "Project-specific careful content");
|
|
104
|
-
|
|
105
|
-
const projectDir = join(tempDir, "project");
|
|
106
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
107
|
-
|
|
108
|
-
// Project snippet should override global
|
|
109
|
-
expect(snippets.get("careful")?.content).toBe("Project-specific careful content");
|
|
110
|
-
expect(snippets.size).toBe(1);
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe("Alias handling", () => {
|
|
115
|
-
it("should handle multiple aliases from different sources", async () => {
|
|
116
|
-
// Global snippet with aliases
|
|
117
|
-
await writeFile(
|
|
118
|
-
join(globalSnippetDir, "review.md"),
|
|
119
|
-
`---
|
|
120
|
-
aliases:
|
|
121
|
-
- pr
|
|
122
|
-
- check
|
|
123
|
-
---
|
|
124
|
-
Global review guidelines`,
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Project snippet with aliases
|
|
128
|
-
await writeFile(
|
|
129
|
-
join(projectSnippetDir, "test.md"),
|
|
130
|
-
`---
|
|
131
|
-
aliases:
|
|
132
|
-
- tdd
|
|
133
|
-
- testing
|
|
134
|
-
---
|
|
135
|
-
Project test guidelines`,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const projectDir = join(tempDir, "project");
|
|
139
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
140
|
-
|
|
141
|
-
expect(snippets.size).toBe(6); // review, pr, check, test, tdd, testing
|
|
142
|
-
expect(snippets.get("review")?.content).toBe("Global review guidelines");
|
|
143
|
-
expect(snippets.get("pr")?.content).toBe("Global review guidelines");
|
|
144
|
-
expect(snippets.get("check")?.content).toBe("Global review guidelines");
|
|
145
|
-
expect(snippets.get("test")?.content).toBe("Project test guidelines");
|
|
146
|
-
expect(snippets.get("tdd")?.content).toBe("Project test guidelines");
|
|
147
|
-
expect(snippets.get("testing")?.content).toBe("Project test guidelines");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it("should allow project to override global aliases", async () => {
|
|
151
|
-
// Global snippet with aliases
|
|
152
|
-
await writeFile(
|
|
153
|
-
join(globalSnippetDir, "careful.md"),
|
|
154
|
-
`---
|
|
155
|
-
aliases:
|
|
156
|
-
- safe
|
|
157
|
-
- cautious
|
|
158
|
-
---
|
|
159
|
-
Global careful`,
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
// Project snippet with same name but different aliases
|
|
163
|
-
await writeFile(
|
|
164
|
-
join(projectSnippetDir, "careful.md"),
|
|
165
|
-
`---
|
|
166
|
-
aliases: safe
|
|
167
|
-
---
|
|
168
|
-
Project careful`,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const projectDir = join(tempDir, "project");
|
|
172
|
-
const snippets = await loadSnippets(projectDir, globalSnippetDir);
|
|
173
|
-
|
|
174
|
-
// Project should override with its aliases
|
|
175
|
-
expect(snippets.get("careful")?.content).toBe("Project careful");
|
|
176
|
-
expect(snippets.get("safe")?.content).toBe("Project careful");
|
|
177
|
-
expect(snippets.get("cautious")?.content).toBeUndefined();
|
|
178
|
-
expect(snippets.size).toBe(2);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe("Edge cases", () => {
|
|
183
|
-
it("should handle empty snippet content", async () => {
|
|
184
|
-
await writeFile(join(globalSnippetDir, "empty.md"), "");
|
|
185
|
-
|
|
186
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
187
|
-
expect(snippets.size).toBe(1);
|
|
188
|
-
expect(snippets.get("empty")?.content).toBe("");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it("should handle snippet with only metadata", async () => {
|
|
192
|
-
await writeFile(
|
|
193
|
-
join(globalSnippetDir, "metadata-only.md"),
|
|
194
|
-
`---
|
|
195
|
-
description: "A snippet with only metadata"
|
|
196
|
-
aliases: meta
|
|
197
|
-
---`,
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
201
|
-
expect(snippets.size).toBe(2);
|
|
202
|
-
expect(snippets.get("metadata-only")?.content).toBe("");
|
|
203
|
-
expect(snippets.get("meta")?.content).toBe("");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("should handle multiline content", async () => {
|
|
207
|
-
await writeFile(
|
|
208
|
-
join(globalSnippetDir, "multiline.md"),
|
|
209
|
-
`Line 1
|
|
210
|
-
Line 2
|
|
211
|
-
Line 3`,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
215
|
-
expect(snippets.get("multiline")?.content).toBe("Line 1\nLine 2\nLine 3");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it("should ignore non-.md files", async () => {
|
|
219
|
-
await writeFile(join(globalSnippetDir, "not-a-snippet.txt"), "This should be ignored");
|
|
220
|
-
await writeFile(join(globalSnippetDir, "valid.md"), "This should be loaded");
|
|
221
|
-
|
|
222
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
223
|
-
expect(snippets.size).toBe(1);
|
|
224
|
-
expect(snippets.get("valid")?.content).toBe("This should be loaded");
|
|
225
|
-
expect(snippets.has("not-a-snippet")).toBe(false);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("should handle invalid frontmatter", async () => {
|
|
229
|
-
await writeFile(
|
|
230
|
-
join(globalSnippetDir, "bad-frontmatter.md"),
|
|
231
|
-
`---
|
|
232
|
-
invalid yaml
|
|
233
|
-
---
|
|
234
|
-
Content`,
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
await writeFile(join(globalSnippetDir, "special-chars.md"), "Special content");
|
|
238
|
-
|
|
239
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
240
|
-
// Should load valid snippet, skip invalid one
|
|
241
|
-
expect(snippets.get("special-chars")?.content).toBe("Special content");
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("should handle non-existent directories gracefully", async () => {
|
|
245
|
-
const snippets = await loadSnippets(undefined, "/nonexistent/path");
|
|
246
|
-
expect(snippets.size).toBe(0);
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe("Smoke test - real global snippets", () => {
|
|
251
|
-
it("should load real global snippets without crashing", async () => {
|
|
252
|
-
// This is a smoke test - just verify it doesn't crash
|
|
253
|
-
const snippets = await loadSnippets();
|
|
254
|
-
|
|
255
|
-
// We don't assert specific content since we don't control user's global snippets
|
|
256
|
-
// Just verify it returns a Map
|
|
257
|
-
expect(snippets).toBeInstanceOf(Map);
|
|
258
|
-
expect(Array.isArray(Array.from(snippets.keys()))).toBe(true);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// PR #13 requirement: accept both 'alias' and 'aliases' in frontmatter
|
|
263
|
-
describe("Frontmatter alias/aliases normalization", () => {
|
|
264
|
-
it("accepts 'alias' as string (singular form)", async () => {
|
|
265
|
-
await writeFile(
|
|
266
|
-
join(globalSnippetDir, "greeting.md"),
|
|
267
|
-
`---
|
|
268
|
-
alias: hi
|
|
269
|
-
---
|
|
270
|
-
Hello there!`,
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
274
|
-
|
|
275
|
-
expect(snippets.size).toBe(2);
|
|
276
|
-
expect(snippets.get("greeting")?.content).toBe("Hello there!");
|
|
277
|
-
expect(snippets.get("hi")?.content).toBe("Hello there!");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("accepts 'alias' as array (singular form)", async () => {
|
|
281
|
-
await writeFile(
|
|
282
|
-
join(globalSnippetDir, "greeting.md"),
|
|
283
|
-
`---
|
|
284
|
-
alias:
|
|
285
|
-
- hi
|
|
286
|
-
- hello
|
|
287
|
-
---
|
|
288
|
-
Hello there!`,
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
292
|
-
|
|
293
|
-
expect(snippets.size).toBe(3);
|
|
294
|
-
expect(snippets.get("greeting")?.content).toBe("Hello there!");
|
|
295
|
-
expect(snippets.get("hi")?.content).toBe("Hello there!");
|
|
296
|
-
expect(snippets.get("hello")?.content).toBe("Hello there!");
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("accepts 'aliases' as string (existing behavior)", async () => {
|
|
300
|
-
await writeFile(
|
|
301
|
-
join(globalSnippetDir, "greeting.md"),
|
|
302
|
-
`---
|
|
303
|
-
aliases: hi
|
|
304
|
-
---
|
|
305
|
-
Hello there!`,
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
309
|
-
|
|
310
|
-
expect(snippets.size).toBe(2);
|
|
311
|
-
expect(snippets.get("greeting")?.content).toBe("Hello there!");
|
|
312
|
-
expect(snippets.get("hi")?.content).toBe("Hello there!");
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("accepts 'aliases' as array (existing behavior)", async () => {
|
|
316
|
-
await writeFile(
|
|
317
|
-
join(globalSnippetDir, "greeting.md"),
|
|
318
|
-
`---
|
|
319
|
-
aliases:
|
|
320
|
-
- hi
|
|
321
|
-
- hello
|
|
322
|
-
---
|
|
323
|
-
Hello there!`,
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
327
|
-
|
|
328
|
-
expect(snippets.size).toBe(3);
|
|
329
|
-
expect(snippets.get("greeting")?.content).toBe("Hello there!");
|
|
330
|
-
expect(snippets.get("hi")?.content).toBe("Hello there!");
|
|
331
|
-
expect(snippets.get("hello")?.content).toBe("Hello there!");
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("prefers 'aliases' over 'alias' if both present", async () => {
|
|
335
|
-
await writeFile(
|
|
336
|
-
join(globalSnippetDir, "greeting.md"),
|
|
337
|
-
`---
|
|
338
|
-
alias: ignored
|
|
339
|
-
aliases: used
|
|
340
|
-
---
|
|
341
|
-
Hello there!`,
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
const snippets = await loadSnippets(undefined, globalSnippetDir);
|
|
345
|
-
|
|
346
|
-
expect(snippets.size).toBe(2);
|
|
347
|
-
expect(snippets.get("greeting")?.content).toBe("Hello there!");
|
|
348
|
-
expect(snippets.get("used")?.content).toBe("Hello there!");
|
|
349
|
-
expect(snippets.has("ignored")).toBe(false);
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
});
|