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/commands.test.ts
DELETED
|
@@ -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;
|