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.
Files changed (56) hide show
  1. package/dist/index.d.ts +11 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +72 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/arg-parser.d.ts +16 -0
  6. package/dist/src/arg-parser.d.ts.map +1 -0
  7. package/dist/src/arg-parser.js +94 -0
  8. package/dist/src/arg-parser.js.map +1 -0
  9. package/dist/src/commands.d.ts +30 -0
  10. package/dist/src/commands.d.ts.map +1 -0
  11. package/dist/src/commands.js +315 -0
  12. package/dist/src/commands.js.map +1 -0
  13. package/dist/src/constants.d.ts +26 -0
  14. package/dist/src/constants.d.ts.map +1 -0
  15. package/dist/src/constants.js +28 -0
  16. package/dist/src/constants.js.map +1 -0
  17. package/dist/src/expander.d.ts +36 -0
  18. package/dist/src/expander.d.ts.map +1 -0
  19. package/dist/src/expander.js +187 -0
  20. package/dist/src/expander.js.map +1 -0
  21. package/dist/src/loader.d.ts +46 -0
  22. package/dist/src/loader.d.ts.map +1 -0
  23. package/dist/src/loader.js +223 -0
  24. package/dist/src/loader.js.map +1 -0
  25. package/dist/src/logger.d.ts +15 -0
  26. package/dist/src/logger.d.ts.map +1 -0
  27. package/dist/src/logger.js +107 -0
  28. package/dist/src/logger.js.map +1 -0
  29. package/dist/src/notification.d.ts +11 -0
  30. package/dist/src/notification.d.ts.map +1 -0
  31. package/dist/src/notification.js +26 -0
  32. package/dist/src/notification.js.map +1 -0
  33. package/dist/src/shell.d.ts +18 -0
  34. package/dist/src/shell.d.ts.map +1 -0
  35. package/dist/src/shell.js +30 -0
  36. package/dist/src/shell.js.map +1 -0
  37. package/dist/src/types.d.ts +65 -0
  38. package/dist/src/types.d.ts.map +1 -0
  39. package/dist/src/types.js +2 -0
  40. package/dist/src/types.js.map +1 -0
  41. package/package.json +8 -5
  42. package/index.ts +0 -81
  43. package/src/arg-parser.test.ts +0 -177
  44. package/src/arg-parser.ts +0 -87
  45. package/src/commands.test.ts +0 -188
  46. package/src/commands.ts +0 -414
  47. package/src/constants.ts +0 -32
  48. package/src/expander.test.ts +0 -846
  49. package/src/expander.ts +0 -225
  50. package/src/loader.test.ts +0 -352
  51. package/src/loader.ts +0 -268
  52. package/src/logger.test.ts +0 -136
  53. package/src/logger.ts +0 -121
  54. package/src/notification.ts +0 -30
  55. package/src/shell.ts +0 -50
  56. package/src/types.ts +0 -71
@@ -1,188 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { parseAddOptions } from "./commands.js";
3
-
4
- describe("parseAddOptions", () => {
5
- // Alias variations - all 4 must work per PR #13 requirements
6
- describe("alias parameter variations", () => {
7
- it("parses --alias=a,b", () => {
8
- expect(parseAddOptions(["--alias=a,b"])).toEqual({
9
- aliases: ["a", "b"],
10
- description: undefined,
11
- isProject: false,
12
- });
13
- });
14
-
15
- it("parses --alias a,b (space-separated)", () => {
16
- expect(parseAddOptions(["--alias", "a,b"])).toEqual({
17
- aliases: ["a", "b"],
18
- description: undefined,
19
- isProject: false,
20
- });
21
- });
22
-
23
- it("parses --aliases=a,b", () => {
24
- expect(parseAddOptions(["--aliases=a,b"])).toEqual({
25
- aliases: ["a", "b"],
26
- description: undefined,
27
- isProject: false,
28
- });
29
- });
30
-
31
- it("parses --aliases a,b (space-separated)", () => {
32
- expect(parseAddOptions(["--aliases", "a,b"])).toEqual({
33
- aliases: ["a", "b"],
34
- description: undefined,
35
- isProject: false,
36
- });
37
- });
38
-
39
- it("parses single alias", () => {
40
- expect(parseAddOptions(["--alias=foo"])).toEqual({
41
- aliases: ["foo"],
42
- description: undefined,
43
- isProject: false,
44
- });
45
- });
46
-
47
- it("handles multiple alias values with spaces", () => {
48
- expect(parseAddOptions(["--aliases=hello, world, foo"])).toEqual({
49
- aliases: ["hello", "world", "foo"],
50
- description: undefined,
51
- isProject: false,
52
- });
53
- });
54
- });
55
-
56
- // Description variations - all must work
57
- describe("description parameter variations", () => {
58
- it("parses --desc=value", () => {
59
- expect(parseAddOptions(["--desc=hello"])).toEqual({
60
- aliases: [],
61
- description: "hello",
62
- isProject: false,
63
- });
64
- });
65
-
66
- it("parses --desc value (space-separated)", () => {
67
- expect(parseAddOptions(["--desc", "hello"])).toEqual({
68
- aliases: [],
69
- description: "hello",
70
- isProject: false,
71
- });
72
- });
73
-
74
- it("parses --desc with apostrophe (main bug)", () => {
75
- expect(parseAddOptions(["--desc=don't break"])).toEqual({
76
- aliases: [],
77
- description: "don't break",
78
- isProject: false,
79
- });
80
- });
81
-
82
- it("parses --description=value", () => {
83
- expect(parseAddOptions(["--description=test"])).toEqual({
84
- aliases: [],
85
- description: "test",
86
- isProject: false,
87
- });
88
- });
89
-
90
- it("parses --description value (space-separated)", () => {
91
- expect(parseAddOptions(["--description", "test"])).toEqual({
92
- aliases: [],
93
- description: "test",
94
- isProject: false,
95
- });
96
- });
97
-
98
- it("parses --desc with multiline content", () => {
99
- expect(parseAddOptions(["--desc=line1\nline2"])).toEqual({
100
- aliases: [],
101
- description: "line1\nline2",
102
- isProject: false,
103
- });
104
- });
105
- });
106
-
107
- // Project flag
108
- describe("--project flag", () => {
109
- it("parses --project flag", () => {
110
- expect(parseAddOptions(["--project"])).toEqual({
111
- aliases: [],
112
- description: undefined,
113
- isProject: true,
114
- });
115
- });
116
-
117
- it("parses --project in any position", () => {
118
- expect(parseAddOptions(["--desc=test", "--project"])).toEqual({
119
- aliases: [],
120
- description: "test",
121
- isProject: true,
122
- });
123
- });
124
- });
125
-
126
- // Combined options
127
- describe("combined options", () => {
128
- it("parses multiple options together", () => {
129
- expect(parseAddOptions(["--alias=a,b", "--desc=hello", "--project"])).toEqual({
130
- aliases: ["a", "b"],
131
- description: "hello",
132
- isProject: true,
133
- });
134
- });
135
-
136
- it("parses all space-separated options together", () => {
137
- expect(parseAddOptions(["--alias", "a,b", "--desc", "hello", "--project"])).toEqual({
138
- aliases: ["a", "b"],
139
- description: "hello",
140
- isProject: true,
141
- });
142
- });
143
-
144
- it("handles mixed = and space syntax", () => {
145
- expect(parseAddOptions(["--alias=a,b", "--desc", "hello"])).toEqual({
146
- aliases: ["a", "b"],
147
- description: "hello",
148
- isProject: false,
149
- });
150
- });
151
- });
152
-
153
- // Edge cases
154
- describe("edge cases", () => {
155
- it("returns defaults for empty args", () => {
156
- expect(parseAddOptions([])).toEqual({
157
- aliases: [],
158
- description: undefined,
159
- isProject: false,
160
- });
161
- });
162
-
163
- it("ignores unknown options", () => {
164
- expect(parseAddOptions(["--unknown=value", "--desc=hello"])).toEqual({
165
- aliases: [],
166
- description: "hello",
167
- isProject: false,
168
- });
169
- });
170
-
171
- it("ignores positional args (non-option)", () => {
172
- expect(parseAddOptions(["positional", "--desc=hello"])).toEqual({
173
- aliases: [],
174
- description: "hello",
175
- isProject: false,
176
- });
177
- });
178
-
179
- it("does not consume value after --project", () => {
180
- // --project is a flag, should not consume next arg
181
- expect(parseAddOptions(["--project", "--desc=hello"])).toEqual({
182
- aliases: [],
183
- description: "hello",
184
- isProject: true,
185
- });
186
- });
187
- });
188
- });
package/src/commands.ts DELETED
@@ -1,414 +0,0 @@
1
- import { parseCommandArgs } from "./arg-parser.js";
2
- import { PATHS } from "./constants.js";
3
- import { createSnippet, deleteSnippet, listSnippets, reloadSnippets } from "./loader.js";
4
- import { logger } from "./logger.js";
5
- import { sendIgnoredMessage } from "./notification.js";
6
- import type { OpencodeClient, SnippetRegistry } from "./types.js";
7
-
8
- /** Marker error to indicate command was handled */
9
- const COMMAND_HANDLED_MARKER = "__SNIPPETS_COMMAND_HANDLED__";
10
-
11
- interface CommandContext {
12
- client: OpencodeClient;
13
- sessionId: string;
14
- args: string[];
15
- rawArguments: string;
16
- snippets: SnippetRegistry;
17
- projectDir?: string;
18
- }
19
-
20
- /**
21
- * Parsed options from the add command arguments
22
- */
23
- export interface AddOptions {
24
- aliases: string[];
25
- description: string | undefined;
26
- isProject: boolean;
27
- }
28
-
29
- /**
30
- * Parses option arguments for the add command.
31
- *
32
- * Supports all variations per PR #13 requirements:
33
- * - --alias=a,b, --alias a,b, --aliases=a,b, --aliases a,b
34
- * - --desc=x, --desc x, --description=x, --description x
35
- * - --project flag
36
- *
37
- * @param args - Array of parsed arguments (after name and content extraction)
38
- * @returns Parsed options object
39
- */
40
- export function parseAddOptions(args: string[]): AddOptions {
41
- const result: AddOptions = {
42
- aliases: [],
43
- description: undefined,
44
- isProject: false,
45
- };
46
-
47
- for (let i = 0; i < args.length; i++) {
48
- const arg = args[i];
49
-
50
- // Skip non-option arguments
51
- if (!arg.startsWith("--")) {
52
- continue;
53
- }
54
-
55
- // Handle --project flag
56
- if (arg === "--project") {
57
- result.isProject = true;
58
- continue;
59
- }
60
-
61
- // Check for --alias or --aliases
62
- if (arg === "--alias" || arg === "--aliases") {
63
- // Space-separated: --alias a,b
64
- const nextArg = args[i + 1];
65
- if (nextArg && !nextArg.startsWith("--")) {
66
- result.aliases = parseAliasValue(nextArg);
67
- i++; // Skip the value arg
68
- }
69
- continue;
70
- }
71
-
72
- if (arg.startsWith("--alias=") || arg.startsWith("--aliases=")) {
73
- // Equals syntax: --alias=a,b
74
- const value = arg.includes("--aliases=")
75
- ? arg.slice("--aliases=".length)
76
- : arg.slice("--alias=".length);
77
- result.aliases = parseAliasValue(value);
78
- continue;
79
- }
80
-
81
- // Check for --desc or --description
82
- if (arg === "--desc" || arg === "--description") {
83
- // Space-separated: --desc value
84
- const nextArg = args[i + 1];
85
- if (nextArg && !nextArg.startsWith("--")) {
86
- result.description = nextArg;
87
- i++; // Skip the value arg
88
- }
89
- continue;
90
- }
91
-
92
- if (arg.startsWith("--desc=") || arg.startsWith("--description=")) {
93
- // Equals syntax: --desc=value
94
- const value = arg.startsWith("--description=")
95
- ? arg.slice("--description=".length)
96
- : arg.slice("--desc=".length);
97
- result.description = value;
98
- }
99
- }
100
-
101
- return result;
102
- }
103
-
104
- /**
105
- * Parse comma-separated alias values, trimming whitespace
106
- */
107
- function parseAliasValue(value: string): string[] {
108
- return value
109
- .split(",")
110
- .map((s) => s.trim())
111
- .filter(Boolean);
112
- }
113
-
114
- /**
115
- * Creates the command execute handler for the snippets command
116
- */
117
- export function createCommandExecuteHandler(
118
- client: OpencodeClient,
119
- snippets: SnippetRegistry,
120
- projectDir?: string,
121
- ) {
122
- return async (input: { command: string; sessionID: string; arguments: string }) => {
123
- if (input.command !== "snippet") return;
124
-
125
- // Use shell-like argument parsing to handle quoted strings correctly
126
- const args = parseCommandArgs(input.arguments);
127
- const subcommand = args[0]?.toLowerCase() || "help";
128
-
129
- const ctx: CommandContext = {
130
- client,
131
- sessionId: input.sessionID,
132
- args: args.slice(1),
133
- rawArguments: input.arguments,
134
- snippets,
135
- projectDir,
136
- };
137
-
138
- try {
139
- switch (subcommand) {
140
- case "add":
141
- case "create":
142
- case "new":
143
- await handleAddCommand(ctx);
144
- break;
145
- case "delete":
146
- case "remove":
147
- case "rm":
148
- await handleDeleteCommand(ctx);
149
- break;
150
- case "list":
151
- case "ls":
152
- await handleListCommand(ctx);
153
- break;
154
- default:
155
- await handleHelpCommand(ctx);
156
- break;
157
- }
158
- } catch (error) {
159
- if (error instanceof Error && error.message === COMMAND_HANDLED_MARKER) {
160
- throw error;
161
- }
162
- logger.error("Command execution failed", {
163
- subcommand,
164
- error: error instanceof Error ? error.message : String(error),
165
- });
166
- await sendIgnoredMessage(
167
- ctx.client,
168
- ctx.sessionId,
169
- `Error: ${error instanceof Error ? error.message : String(error)}`,
170
- );
171
- }
172
-
173
- // Signal that command was handled
174
- throw new Error(COMMAND_HANDLED_MARKER);
175
- };
176
- }
177
-
178
- /**
179
- * Handle /snippet add <name> ["content"] [--project] [--alias=<alias>] [--desc=<description>]
180
- */
181
- async function handleAddCommand(ctx: CommandContext): Promise<void> {
182
- const { client, sessionId, args, snippets, projectDir } = ctx;
183
-
184
- if (args.length === 0) {
185
- await sendIgnoredMessage(
186
- client,
187
- sessionId,
188
- 'Usage: /snippet add <name> ["content"] [options]\n\n' +
189
- "Adds a new snippet. Defaults to global directory.\n\n" +
190
- "Examples:\n" +
191
- " /snippet add greeting\n" +
192
- ' /snippet add bye "see you later"\n' +
193
- ' /snippet add hi "hello there" --aliases hello,hey\n' +
194
- ' /snippet add fix "fix imports" --project\n\n' +
195
- "Options:\n" +
196
- " --project Add to project directory (.opencode/snippet/)\n" +
197
- " --aliases X,Y,Z Add aliases (comma-separated)\n" +
198
- ' --desc "..." Add a description',
199
- );
200
- return;
201
- }
202
-
203
- const name = args[0];
204
-
205
- // Extract content: second argument if it doesn't start with --
206
- // The arg-parser already handles quoted strings, so content is clean
207
- let content = "";
208
- let optionArgs = args.slice(1);
209
-
210
- if (args[1] && !args[1].startsWith("--")) {
211
- content = args[1];
212
- optionArgs = args.slice(2);
213
- }
214
-
215
- // Parse all options using the new parser
216
- const options = parseAddOptions(optionArgs);
217
-
218
- // Default to global, --project puts it in project directory
219
- const targetDir = options.isProject ? projectDir : undefined;
220
- const location = options.isProject && projectDir ? "project" : "global";
221
-
222
- try {
223
- const filePath = await createSnippet(
224
- name,
225
- content,
226
- { aliases: options.aliases, description: options.description },
227
- targetDir,
228
- );
229
-
230
- // Reload snippets
231
- await reloadSnippets(snippets, projectDir);
232
-
233
- let message = `Added ${location} snippet: ${name}\nFile: ${filePath}`;
234
- if (content) {
235
- message += `\nContent: "${truncate(content, 50)}"`;
236
- } else {
237
- message += "\n\nEdit the file to add your snippet content.";
238
- }
239
- if (options.aliases.length > 0) {
240
- message += `\nAliases: ${options.aliases.join(", ")}`;
241
- }
242
-
243
- await sendIgnoredMessage(client, sessionId, message);
244
- } catch (error) {
245
- await sendIgnoredMessage(
246
- client,
247
- sessionId,
248
- `Failed to add snippet: ${error instanceof Error ? error.message : String(error)}`,
249
- );
250
- }
251
- }
252
-
253
- /**
254
- * Handle /snippet delete <name>
255
- */
256
- async function handleDeleteCommand(ctx: CommandContext): Promise<void> {
257
- const { client, sessionId, args, snippets, projectDir } = ctx;
258
-
259
- if (args.length === 0) {
260
- await sendIgnoredMessage(
261
- client,
262
- sessionId,
263
- "Usage: /snippet delete <name>\n\nDeletes a snippet by name. " +
264
- "Project snippets are checked first, then global.",
265
- );
266
- return;
267
- }
268
-
269
- const name = args[0];
270
-
271
- const deletedPath = await deleteSnippet(name, projectDir);
272
-
273
- if (deletedPath) {
274
- // Reload snippets
275
- await reloadSnippets(snippets, projectDir);
276
-
277
- await sendIgnoredMessage(
278
- client,
279
- sessionId,
280
- `Deleted snippet: #${name}\nRemoved: ${deletedPath}`,
281
- );
282
- } else {
283
- await sendIgnoredMessage(
284
- client,
285
- sessionId,
286
- `Snippet not found: #${name}\n\nUse /snippet list to see available snippets.`,
287
- );
288
- }
289
- }
290
-
291
- /** Maximum characters for snippet content preview */
292
- const MAX_CONTENT_PREVIEW_LENGTH = 200;
293
- /** Maximum characters for aliases display */
294
- const MAX_ALIASES_LENGTH = 50;
295
- /** Divider line */
296
- const DIVIDER = "────────────────────────────────────────────────";
297
-
298
- /**
299
- * Truncate text with ellipsis if it exceeds maxLength
300
- */
301
- function truncate(text: string, maxLength: number): string {
302
- if (text.length <= maxLength) return text;
303
- return `${text.slice(0, maxLength - 3)}...`;
304
- }
305
-
306
- /**
307
- * Format aliases for display, truncating if needed
308
- */
309
- function formatAliases(aliases: string[]): string {
310
- if (aliases.length === 0) return "";
311
-
312
- const joined = aliases.join(", ");
313
- if (joined.length <= MAX_ALIASES_LENGTH) {
314
- return ` (aliases: ${joined})`;
315
- }
316
-
317
- // Truncate and show count
318
- const truncated = truncate(joined, MAX_ALIASES_LENGTH - 10);
319
- return ` (aliases: ${truncated} +${aliases.length})`;
320
- }
321
-
322
- /**
323
- * Format a single snippet for display
324
- */
325
- function formatSnippetEntry(s: { name: string; content: string; aliases: string[] }): string {
326
- const header = `${s.name}${formatAliases(s.aliases)}`;
327
- const content = truncate(s.content.trim(), MAX_CONTENT_PREVIEW_LENGTH);
328
-
329
- return `${header}\n${DIVIDER}\n${content || "(empty)"}`;
330
- }
331
-
332
- /**
333
- * Handle /snippet list
334
- */
335
- async function handleListCommand(ctx: CommandContext): Promise<void> {
336
- const { client, sessionId, snippets, projectDir } = ctx;
337
-
338
- const snippetList = listSnippets(snippets);
339
-
340
- if (snippetList.length === 0) {
341
- await sendIgnoredMessage(
342
- client,
343
- sessionId,
344
- "No snippets found.\n\n" +
345
- `Global snippets: ${PATHS.SNIPPETS_DIR}\n` +
346
- (projectDir
347
- ? `Project snippets: ${projectDir}/.opencode/snippet/`
348
- : "No project directory detected.") +
349
- "\n\nUse /snippet add <name> to add a new snippet.",
350
- );
351
- return;
352
- }
353
-
354
- const lines: string[] = [];
355
-
356
- // Group by source
357
- const globalSnippets = snippetList.filter((s) => s.source === "global");
358
- const projectSnippets = snippetList.filter((s) => s.source === "project");
359
-
360
- if (globalSnippets.length > 0) {
361
- lines.push(`── Global (${PATHS.SNIPPETS_DIR}) ──`, "");
362
- for (const s of globalSnippets) {
363
- lines.push(formatSnippetEntry(s), "");
364
- }
365
- }
366
-
367
- if (projectSnippets.length > 0) {
368
- lines.push(`── Project (${projectDir}/.opencode/snippet/) ──`, "");
369
- for (const s of projectSnippets) {
370
- lines.push(formatSnippetEntry(s), "");
371
- }
372
- }
373
-
374
- await sendIgnoredMessage(client, sessionId, lines.join("\n").trimEnd());
375
- }
376
-
377
- /**
378
- * Handle /snippet help
379
- */
380
- async function handleHelpCommand(ctx: CommandContext): Promise<void> {
381
- const { client, sessionId } = ctx;
382
-
383
- const helpText = `Snippets Command - Manage text snippets
384
-
385
- Usage: /snippet <command> [options]
386
-
387
- Commands:
388
- add <name> ["content"] [options]
389
- --project Add to project directory (default: global)
390
- --aliases X,Y,Z Add aliases (comma-separated)
391
- --desc "..." Add a description
392
-
393
- delete <name> Delete a snippet
394
- list List all available snippets
395
- help Show this help message
396
-
397
- Snippet Locations:
398
- Global: ~/.config/opencode/snippet/
399
- Project: <project>/.opencode/snippet/
400
-
401
- Usage in messages:
402
- Type #snippet-name to expand a snippet inline.
403
- Snippets can reference other snippets recursively.
404
-
405
- Examples:
406
- /snippet add greeting
407
- /snippet add bye "see you later"
408
- /snippet add hi "hello there" --aliases hello,hey
409
- /snippet add fix "fix imports" --project
410
- /snippet delete old-snippet
411
- /snippet list`;
412
-
413
- await sendIgnoredMessage(client, sessionId, helpText);
414
- }
package/src/constants.ts DELETED
@@ -1,32 +0,0 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
-
4
- /**
5
- * Regular expression patterns used throughout the plugin
6
- */
7
- export const PATTERNS = {
8
- /** Matches hashtags like #snippet-name */
9
- HASHTAG: /#([a-z0-9\-_]+)/gi,
10
-
11
- /** Matches shell commands like !`command` */
12
- SHELL_COMMAND: /!`([^`]+)`/g,
13
- } as const;
14
-
15
- /**
16
- * File system paths
17
- */
18
- export const PATHS = {
19
- /** OpenCode configuration directory */
20
- CONFIG_DIR: join(homedir(), ".config", "opencode"),
21
-
22
- /** Snippets directory */
23
- SNIPPETS_DIR: join(join(homedir(), ".config", "opencode"), "snippet"),
24
- } as const;
25
-
26
- /**
27
- * Plugin configuration
28
- */
29
- export const CONFIG = {
30
- /** File extension for snippet files */
31
- SNIPPET_EXTENSION: ".md",
32
- } as const;