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/loader.ts
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
3
|
-
import matter from "gray-matter";
|
|
4
|
-
import { CONFIG, PATHS } from "./constants.js";
|
|
5
|
-
import { logger } from "./logger.js";
|
|
6
|
-
import type { SnippetFrontmatter, SnippetInfo, SnippetRegistry } from "./types.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Loads all snippets from global and project directories
|
|
10
|
-
*
|
|
11
|
-
* @param projectDir - Optional project directory path (from ctx.directory)
|
|
12
|
-
* @param globalDir - Optional global snippets directory (for testing)
|
|
13
|
-
* @returns A map of snippet keys (lowercase) to their SnippetInfo
|
|
14
|
-
*/
|
|
15
|
-
export async function loadSnippets(
|
|
16
|
-
projectDir?: string,
|
|
17
|
-
globalDir?: string,
|
|
18
|
-
): Promise<SnippetRegistry> {
|
|
19
|
-
const snippets: SnippetRegistry = new Map();
|
|
20
|
-
|
|
21
|
-
// Load from global directory first (use provided or default)
|
|
22
|
-
const globalSnippetsDir = globalDir ?? PATHS.SNIPPETS_DIR;
|
|
23
|
-
await loadFromDirectory(globalSnippetsDir, snippets, "global");
|
|
24
|
-
|
|
25
|
-
// Load from project directory if provided (overrides global)
|
|
26
|
-
if (projectDir) {
|
|
27
|
-
const projectSnippetsDir = join(projectDir, ".opencode", "snippet");
|
|
28
|
-
await loadFromDirectory(projectSnippetsDir, snippets, "project");
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return snippets;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Loads snippets from a specific directory
|
|
36
|
-
*
|
|
37
|
-
* @param dir - Directory to load snippets from
|
|
38
|
-
* @param registry - Registry to populate
|
|
39
|
-
* @param source - Source label for logging
|
|
40
|
-
*/
|
|
41
|
-
async function loadFromDirectory(
|
|
42
|
-
dir: string,
|
|
43
|
-
registry: SnippetRegistry,
|
|
44
|
-
source: "global" | "project",
|
|
45
|
-
): Promise<void> {
|
|
46
|
-
try {
|
|
47
|
-
const files = await readdir(dir);
|
|
48
|
-
|
|
49
|
-
for (const file of files) {
|
|
50
|
-
if (!file.endsWith(CONFIG.SNIPPET_EXTENSION)) continue;
|
|
51
|
-
|
|
52
|
-
const snippet = await loadSnippetFile(dir, file, source);
|
|
53
|
-
if (snippet) {
|
|
54
|
-
registerSnippet(registry, snippet);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
logger.debug(`Loaded snippets from ${source} directory`, {
|
|
59
|
-
path: dir,
|
|
60
|
-
fileCount: files.length,
|
|
61
|
-
});
|
|
62
|
-
} catch (error) {
|
|
63
|
-
// Snippets directory doesn't exist or can't be read - that's fine
|
|
64
|
-
logger.debug(`${source} snippets directory not found or unreadable`, {
|
|
65
|
-
path: dir,
|
|
66
|
-
error: error instanceof Error ? error.message : String(error),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Loads and parses a single snippet file
|
|
73
|
-
*
|
|
74
|
-
* @param dir - Directory containing the snippet file
|
|
75
|
-
* @param filename - The filename to load (e.g., "my-snippet.md")
|
|
76
|
-
* @param source - Whether this is a global or project snippet
|
|
77
|
-
* @returns The parsed snippet info, or null if parsing failed
|
|
78
|
-
*/
|
|
79
|
-
async function loadSnippetFile(
|
|
80
|
-
dir: string,
|
|
81
|
-
filename: string,
|
|
82
|
-
source: "global" | "project",
|
|
83
|
-
): Promise<SnippetInfo | null> {
|
|
84
|
-
try {
|
|
85
|
-
const name = basename(filename, CONFIG.SNIPPET_EXTENSION);
|
|
86
|
-
const filePath = join(dir, filename);
|
|
87
|
-
const fileContent = await readFile(filePath, "utf-8");
|
|
88
|
-
const parsed = matter(fileContent);
|
|
89
|
-
|
|
90
|
-
const content = parsed.content.trim();
|
|
91
|
-
const frontmatter = parsed.data as SnippetFrontmatter;
|
|
92
|
-
|
|
93
|
-
// Handle aliases: accept both 'aliases' (plural) and 'alias' (singular)
|
|
94
|
-
// Prefer 'aliases' if both are present
|
|
95
|
-
let aliases: string[] = [];
|
|
96
|
-
const aliasSource = frontmatter.aliases ?? frontmatter.alias;
|
|
97
|
-
if (aliasSource) {
|
|
98
|
-
if (Array.isArray(aliasSource)) {
|
|
99
|
-
aliases = aliasSource;
|
|
100
|
-
} else {
|
|
101
|
-
aliases = [aliasSource];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return {
|
|
106
|
-
name,
|
|
107
|
-
content,
|
|
108
|
-
aliases,
|
|
109
|
-
description: frontmatter.description,
|
|
110
|
-
filePath,
|
|
111
|
-
source,
|
|
112
|
-
};
|
|
113
|
-
} catch (error) {
|
|
114
|
-
// Failed to read or parse this snippet - skip it
|
|
115
|
-
logger.warn("Failed to load snippet file", {
|
|
116
|
-
filename,
|
|
117
|
-
error: error instanceof Error ? error.message : String(error),
|
|
118
|
-
});
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Registers a snippet and its aliases in the registry
|
|
125
|
-
*
|
|
126
|
-
* @param registry - The registry to add the snippet to
|
|
127
|
-
* @param snippet - The snippet info to register
|
|
128
|
-
*/
|
|
129
|
-
function registerSnippet(registry: SnippetRegistry, snippet: SnippetInfo): void {
|
|
130
|
-
const key = snippet.name.toLowerCase();
|
|
131
|
-
|
|
132
|
-
// If snippet with same name exists, remove its old aliases first
|
|
133
|
-
const existing = registry.get(key);
|
|
134
|
-
if (existing) {
|
|
135
|
-
for (const alias of existing.aliases) {
|
|
136
|
-
registry.delete(alias.toLowerCase());
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Register the snippet under its name
|
|
141
|
-
registry.set(key, snippet);
|
|
142
|
-
|
|
143
|
-
// Register under all aliases (pointing to the same snippet info)
|
|
144
|
-
for (const alias of snippet.aliases) {
|
|
145
|
-
registry.set(alias.toLowerCase(), snippet);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Lists all unique snippets (by name) from the registry
|
|
151
|
-
*
|
|
152
|
-
* @param registry - The snippet registry
|
|
153
|
-
* @returns Array of unique snippet info objects
|
|
154
|
-
*/
|
|
155
|
-
export function listSnippets(registry: SnippetRegistry): SnippetInfo[] {
|
|
156
|
-
const seen = new Set<string>();
|
|
157
|
-
const snippets: SnippetInfo[] = [];
|
|
158
|
-
|
|
159
|
-
for (const snippet of registry.values()) {
|
|
160
|
-
if (!seen.has(snippet.name)) {
|
|
161
|
-
seen.add(snippet.name);
|
|
162
|
-
snippets.push(snippet);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return snippets;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Ensures the snippets directory exists
|
|
171
|
-
*/
|
|
172
|
-
export async function ensureSnippetsDir(projectDir?: string): Promise<string> {
|
|
173
|
-
const dir = projectDir ? join(projectDir, ".opencode", "snippet") : PATHS.SNIPPETS_DIR;
|
|
174
|
-
await mkdir(dir, { recursive: true });
|
|
175
|
-
return dir;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Creates a new snippet file
|
|
180
|
-
*
|
|
181
|
-
* @param name - The snippet name (without extension)
|
|
182
|
-
* @param content - The snippet content
|
|
183
|
-
* @param options - Optional metadata (aliases, description)
|
|
184
|
-
* @param projectDir - If provided, creates in project directory; otherwise global
|
|
185
|
-
* @returns The path to the created snippet file
|
|
186
|
-
*/
|
|
187
|
-
export async function createSnippet(
|
|
188
|
-
name: string,
|
|
189
|
-
content: string,
|
|
190
|
-
options: { aliases?: string[]; description?: string } = {},
|
|
191
|
-
projectDir?: string,
|
|
192
|
-
): Promise<string> {
|
|
193
|
-
const dir = await ensureSnippetsDir(projectDir);
|
|
194
|
-
const filePath = join(dir, `${name}${CONFIG.SNIPPET_EXTENSION}`);
|
|
195
|
-
|
|
196
|
-
// Build frontmatter if we have metadata
|
|
197
|
-
const frontmatter: SnippetFrontmatter = {};
|
|
198
|
-
if (options.aliases?.length) {
|
|
199
|
-
frontmatter.aliases = options.aliases;
|
|
200
|
-
}
|
|
201
|
-
if (options.description) {
|
|
202
|
-
frontmatter.description = options.description;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Create file content with frontmatter if needed
|
|
206
|
-
let fileContent: string;
|
|
207
|
-
if (Object.keys(frontmatter).length > 0) {
|
|
208
|
-
fileContent = matter.stringify(content, frontmatter);
|
|
209
|
-
} else {
|
|
210
|
-
fileContent = content;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
await writeFile(filePath, fileContent, "utf-8");
|
|
214
|
-
logger.info("Created snippet", { name, path: filePath });
|
|
215
|
-
|
|
216
|
-
return filePath;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Deletes a snippet file
|
|
221
|
-
*
|
|
222
|
-
* @param name - The snippet name (without extension)
|
|
223
|
-
* @param projectDir - If provided, looks in project directory first; otherwise global
|
|
224
|
-
* @returns The path of the deleted file, or null if not found
|
|
225
|
-
*/
|
|
226
|
-
export async function deleteSnippet(name: string, projectDir?: string): Promise<string | null> {
|
|
227
|
-
// Try project directory first if provided
|
|
228
|
-
if (projectDir) {
|
|
229
|
-
const projectPath = join(
|
|
230
|
-
projectDir,
|
|
231
|
-
".opencode",
|
|
232
|
-
"snippet",
|
|
233
|
-
`${name}${CONFIG.SNIPPET_EXTENSION}`,
|
|
234
|
-
);
|
|
235
|
-
try {
|
|
236
|
-
await unlink(projectPath);
|
|
237
|
-
logger.info("Deleted project snippet", { name, path: projectPath });
|
|
238
|
-
return projectPath;
|
|
239
|
-
} catch {
|
|
240
|
-
// Not found in project, try global
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Try global directory
|
|
245
|
-
const globalPath = join(PATHS.SNIPPETS_DIR, `${name}${CONFIG.SNIPPET_EXTENSION}`);
|
|
246
|
-
try {
|
|
247
|
-
await unlink(globalPath);
|
|
248
|
-
logger.info("Deleted global snippet", { name, path: globalPath });
|
|
249
|
-
return globalPath;
|
|
250
|
-
} catch {
|
|
251
|
-
logger.warn("Snippet not found for deletion", { name });
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Reloads snippets into the registry from disk
|
|
258
|
-
*/
|
|
259
|
-
export async function reloadSnippets(
|
|
260
|
-
registry: SnippetRegistry,
|
|
261
|
-
projectDir?: string,
|
|
262
|
-
): Promise<void> {
|
|
263
|
-
registry.clear();
|
|
264
|
-
const fresh = await loadSnippets(projectDir);
|
|
265
|
-
for (const [key, value] of fresh) {
|
|
266
|
-
registry.set(key, value);
|
|
267
|
-
}
|
|
268
|
-
}
|
package/src/logger.test.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { existsSync, mkdtempSync, readFileSync, rmSync } from "fs";
|
|
3
|
-
import { tmpdir } from "os";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
|
|
6
|
-
describe("Logger", () => {
|
|
7
|
-
let tempDir: string;
|
|
8
|
-
let originalEnv: NodeJS.ProcessEnv;
|
|
9
|
-
let Logger: typeof import("../src/logger.js").Logger;
|
|
10
|
-
|
|
11
|
-
beforeAll(() => {
|
|
12
|
-
originalEnv = { ...process.env };
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tempDir = mkdtempSync(join(tmpdir(), "snippets-logger-test-"));
|
|
17
|
-
Logger = require("../src/logger.js").Logger;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
-
vi.restoreAllMocks();
|
|
23
|
-
process.env = originalEnv;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe("isDebugEnabled", () => {
|
|
27
|
-
it("should return false when DEBUG_SNIPPETS is not set", () => {
|
|
28
|
-
delete process.env.DEBUG_SNIPPETS;
|
|
29
|
-
const testLogger = new Logger(join(tempDir, "logs"));
|
|
30
|
-
expect(testLogger.enabled).toBe(false);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("should return true when DEBUG_SNIPPETS=1", () => {
|
|
34
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
35
|
-
const testLogger = new Logger(join(tempDir, "logs"));
|
|
36
|
-
expect(testLogger.enabled).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("should return true when DEBUG_SNIPPETS=true", () => {
|
|
40
|
-
process.env.DEBUG_SNIPPETS = "true";
|
|
41
|
-
const testLogger = new Logger(join(tempDir, "logs"));
|
|
42
|
-
expect(testLogger.enabled).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("should return false for other values", () => {
|
|
46
|
-
process.env.DEBUG_SNIPPETS = "yes";
|
|
47
|
-
const testLogger = new Logger(join(tempDir, "logs"));
|
|
48
|
-
expect(testLogger.enabled).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe("log writing", () => {
|
|
53
|
-
it("should not write logs when debug is disabled", () => {
|
|
54
|
-
delete process.env.DEBUG_SNIPPETS;
|
|
55
|
-
const writeSpy = vi.spyOn(require("fs"), "writeFileSync");
|
|
56
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
57
|
-
logger.debug("test message", { key: "value" });
|
|
58
|
-
expect(writeSpy).not.toHaveBeenCalled();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("should write logs when debug is enabled", () => {
|
|
62
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
63
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
64
|
-
logger.debug("test debug message", { test: true });
|
|
65
|
-
logger.info("test info message");
|
|
66
|
-
logger.warn("test warn message");
|
|
67
|
-
logger.error("test error message");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("should format data correctly", () => {
|
|
71
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
72
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
73
|
-
logger.debug("with data", {
|
|
74
|
-
string: "hello",
|
|
75
|
-
number: 42,
|
|
76
|
-
bool: true,
|
|
77
|
-
nullVal: null,
|
|
78
|
-
undefinedVal: undefined,
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it("should handle empty data", () => {
|
|
83
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
84
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
85
|
-
logger.debug("no data");
|
|
86
|
-
logger.debug("empty object", {});
|
|
87
|
-
logger.debug("empty array", { items: [] as unknown[] });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should format arrays compactly", () => {
|
|
91
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
92
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
93
|
-
logger.debug("small array", { items: [1, 2, 3] });
|
|
94
|
-
logger.debug("large array", { items: [1, 2, 3, 4, 5, 6] });
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("should handle objects in data", () => {
|
|
98
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
99
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
100
|
-
logger.debug("nested object", {
|
|
101
|
-
nested: { a: 1, b: 2 },
|
|
102
|
-
long: "this is a very long string that should be truncated if over 50 characters",
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe("file output", () => {
|
|
108
|
-
it("should create log file in daily directory", () => {
|
|
109
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
110
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
111
|
-
logger.info("file output test", { test: true });
|
|
112
|
-
|
|
113
|
-
const today = new Date().toISOString().split("T")[0];
|
|
114
|
-
const logFile = join(tempDir, "logs", "daily", `${today}.log`);
|
|
115
|
-
|
|
116
|
-
expect(existsSync(logFile)).toBe(true);
|
|
117
|
-
const content = readFileSync(logFile, "utf-8");
|
|
118
|
-
expect(content).toContain("file output test");
|
|
119
|
-
expect(content).toContain("test=true");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it("should append to existing log file", () => {
|
|
123
|
-
process.env.DEBUG_SNIPPETS = "1";
|
|
124
|
-
const logger = new Logger(join(tempDir, "logs"));
|
|
125
|
-
logger.info("first message");
|
|
126
|
-
logger.info("second message");
|
|
127
|
-
|
|
128
|
-
const today = new Date().toISOString().split("T")[0];
|
|
129
|
-
const logFile = join(tempDir, "logs", "daily", `${today}.log`);
|
|
130
|
-
const content = readFileSync(logFile, "utf-8");
|
|
131
|
-
|
|
132
|
-
expect(content).toContain("first message");
|
|
133
|
-
expect(content).toContain("second message");
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
});
|
package/src/logger.ts
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { PATHS } from "./constants.js";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Check if debug logging is enabled via environment variable
|
|
7
|
-
*/
|
|
8
|
-
function isDebugEnabled(): boolean {
|
|
9
|
-
const value = process.env.DEBUG_SNIPPETS;
|
|
10
|
-
return value === "1" || value === "true";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export class Logger {
|
|
14
|
-
private logDir: string;
|
|
15
|
-
|
|
16
|
-
constructor(logDirOverride?: string) {
|
|
17
|
-
this.logDir = logDirOverride ?? join(PATHS.CONFIG_DIR, "logs", "snippets");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
get enabled(): boolean {
|
|
21
|
-
return isDebugEnabled();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private ensureLogDir() {
|
|
25
|
-
if (!existsSync(this.logDir)) {
|
|
26
|
-
mkdirSync(this.logDir, { recursive: true });
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
private formatData(data?: Record<string, unknown>): string {
|
|
31
|
-
if (!data) return "";
|
|
32
|
-
|
|
33
|
-
const parts: string[] = [];
|
|
34
|
-
for (const [key, value] of Object.entries(data)) {
|
|
35
|
-
if (value === undefined || value === null) continue;
|
|
36
|
-
|
|
37
|
-
// Format arrays compactly
|
|
38
|
-
if (Array.isArray(value)) {
|
|
39
|
-
if (value.length === 0) continue;
|
|
40
|
-
parts.push(
|
|
41
|
-
`${key}=[${value.slice(0, 3).join(",")}${value.length > 3 ? `...+${value.length - 3}` : ""}]`,
|
|
42
|
-
);
|
|
43
|
-
} else if (typeof value === "object") {
|
|
44
|
-
const str = JSON.stringify(value);
|
|
45
|
-
if (str.length < 50) {
|
|
46
|
-
parts.push(`${key}=${str}`);
|
|
47
|
-
}
|
|
48
|
-
} else {
|
|
49
|
-
parts.push(`${key}=${value}`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return parts.join(" ");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
private getCallerFile(): string {
|
|
56
|
-
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
57
|
-
try {
|
|
58
|
-
const err = new Error();
|
|
59
|
-
Error.prepareStackTrace = (_, stack) => stack;
|
|
60
|
-
const stack = err.stack as unknown as NodeJS.CallSite[];
|
|
61
|
-
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
62
|
-
|
|
63
|
-
for (let i = 3; i < stack.length; i++) {
|
|
64
|
-
const filename = stack[i]?.getFileName();
|
|
65
|
-
if (filename && !filename.includes("logger.")) {
|
|
66
|
-
const match = filename.match(/([^/\\]+)\.[tj]s$/);
|
|
67
|
-
return match ? match[1] : "unknown";
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return "unknown";
|
|
71
|
-
} catch {
|
|
72
|
-
return "unknown";
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private write(level: string, component: string, message: string, data?: Record<string, unknown>) {
|
|
77
|
-
if (!this.enabled) return;
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
this.ensureLogDir();
|
|
81
|
-
|
|
82
|
-
const timestamp = new Date().toISOString();
|
|
83
|
-
const dataStr = this.formatData(data);
|
|
84
|
-
|
|
85
|
-
const dailyLogDir = join(this.logDir, "daily");
|
|
86
|
-
if (!existsSync(dailyLogDir)) {
|
|
87
|
-
mkdirSync(dailyLogDir, { recursive: true });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? ` | ${dataStr}` : ""}\n`;
|
|
91
|
-
|
|
92
|
-
const logFile = join(dailyLogDir, `${new Date().toISOString().split("T")[0]}.log`);
|
|
93
|
-
writeFileSync(logFile, logLine, { flag: "a" });
|
|
94
|
-
} catch {
|
|
95
|
-
// Silent fail
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
info(message: string, data?: Record<string, unknown>) {
|
|
100
|
-
const component = this.getCallerFile();
|
|
101
|
-
this.write("INFO", component, message, data);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
debug(message: string, data?: Record<string, unknown>) {
|
|
105
|
-
const component = this.getCallerFile();
|
|
106
|
-
this.write("DEBUG", component, message, data);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
warn(message: string, data?: Record<string, unknown>) {
|
|
110
|
-
const component = this.getCallerFile();
|
|
111
|
-
this.write("WARN", component, message, data);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
error(message: string, data?: Record<string, unknown>) {
|
|
115
|
-
const component = this.getCallerFile();
|
|
116
|
-
this.write("ERROR", component, message, data);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Export singleton logger instance
|
|
121
|
-
export const logger = new Logger();
|
package/src/notification.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { logger } from "./logger.js";
|
|
2
|
-
import type { OpencodeClient } from "./types.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Sends a message that will be displayed but ignored by the AI
|
|
6
|
-
* Used for command output that shouldn't trigger AI responses
|
|
7
|
-
*
|
|
8
|
-
* @param client - The OpenCode client instance
|
|
9
|
-
* @param sessionId - The current session ID
|
|
10
|
-
* @param text - The text to display
|
|
11
|
-
*/
|
|
12
|
-
export async function sendIgnoredMessage(
|
|
13
|
-
client: OpencodeClient,
|
|
14
|
-
sessionId: string,
|
|
15
|
-
text: string,
|
|
16
|
-
): Promise<void> {
|
|
17
|
-
try {
|
|
18
|
-
await client.session.prompt({
|
|
19
|
-
path: { id: sessionId },
|
|
20
|
-
body: {
|
|
21
|
-
noReply: true,
|
|
22
|
-
parts: [{ type: "text", text, ignored: true }],
|
|
23
|
-
},
|
|
24
|
-
});
|
|
25
|
-
} catch (error) {
|
|
26
|
-
logger.error("Failed to send ignored message", {
|
|
27
|
-
error: error instanceof Error ? error.message : String(error),
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/shell.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { PATTERNS } from "./constants.js";
|
|
2
|
-
import { logger } from "./logger.js";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Executes shell commands in text using !`command` syntax
|
|
6
|
-
*
|
|
7
|
-
* @param text - The text containing shell commands to execute
|
|
8
|
-
* @param ctx - The plugin context (with Bun shell)
|
|
9
|
-
* @returns The text with shell commands replaced by their output
|
|
10
|
-
*/
|
|
11
|
-
export type ShellContext = {
|
|
12
|
-
$: (
|
|
13
|
-
template: TemplateStringsArray,
|
|
14
|
-
...args: unknown[]
|
|
15
|
-
) => {
|
|
16
|
-
quiet: () => { nothrow: () => { text: () => Promise<string> } };
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export async function executeShellCommands(text: string, ctx: ShellContext): Promise<string> {
|
|
21
|
-
let result = text;
|
|
22
|
-
|
|
23
|
-
// Reset regex state (global flag requires this)
|
|
24
|
-
PATTERNS.SHELL_COMMAND.lastIndex = 0;
|
|
25
|
-
|
|
26
|
-
// Find all shell command matches
|
|
27
|
-
const matches = [...text.matchAll(PATTERNS.SHELL_COMMAND)];
|
|
28
|
-
|
|
29
|
-
// Execute each command and replace in text
|
|
30
|
-
for (const match of matches) {
|
|
31
|
-
const cmd = match[1];
|
|
32
|
-
const _placeholder = match[0];
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const output = await ctx.$`${{ raw: cmd }}`.quiet().nothrow().text();
|
|
36
|
-
// Deviate from slash commands' substitution mechanism: print command first, then output
|
|
37
|
-
const replacement = `$ ${cmd}\n--> ${output.trim()}`;
|
|
38
|
-
result = result.replace(_placeholder, replacement);
|
|
39
|
-
} catch (error) {
|
|
40
|
-
// If shell command fails, leave it as-is
|
|
41
|
-
// This preserves the original syntax for debugging
|
|
42
|
-
logger.warn("Shell command execution failed", {
|
|
43
|
-
command: cmd,
|
|
44
|
-
error: error instanceof Error ? error.message : String(error),
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return result;
|
|
50
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* OpenCode client type from the SDK
|
|
5
|
-
*/
|
|
6
|
-
export type OpencodeClient = ReturnType<typeof createOpencodeClient>;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A snippet with its content and metadata
|
|
10
|
-
*/
|
|
11
|
-
export interface Snippet {
|
|
12
|
-
/** The primary name/key of the snippet */
|
|
13
|
-
name: string;
|
|
14
|
-
/** The content of the snippet (without frontmatter) */
|
|
15
|
-
content: string;
|
|
16
|
-
/** Alternative names that also trigger this snippet */
|
|
17
|
-
aliases: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extended snippet info with file metadata
|
|
22
|
-
*/
|
|
23
|
-
export interface SnippetInfo {
|
|
24
|
-
name: string;
|
|
25
|
-
content: string;
|
|
26
|
-
aliases: string[];
|
|
27
|
-
description?: string;
|
|
28
|
-
filePath: string;
|
|
29
|
-
source: "global" | "project";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Snippet registry that maps keys to snippet info
|
|
34
|
-
*/
|
|
35
|
-
export type SnippetRegistry = Map<string, SnippetInfo>;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Frontmatter data from snippet files
|
|
39
|
-
*/
|
|
40
|
-
export interface SnippetFrontmatter {
|
|
41
|
-
/** Alternative hashtags for this snippet (plural form, preferred) */
|
|
42
|
-
aliases?: string | string[];
|
|
43
|
-
/** Alternative hashtags for this snippet (singular form, also accepted) */
|
|
44
|
-
alias?: string | string[];
|
|
45
|
-
/** Optional description of what this snippet does */
|
|
46
|
-
description?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parsed snippet content with inline text and prepend/append blocks
|
|
51
|
-
*/
|
|
52
|
-
export interface ParsedSnippetContent {
|
|
53
|
-
/** Content outside blocks (replaces hashtag inline) */
|
|
54
|
-
inline: string;
|
|
55
|
-
/** <prepend> block contents in document order */
|
|
56
|
-
prepend: string[];
|
|
57
|
-
/** <append> block contents in document order */
|
|
58
|
-
append: string[];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Result of expanding hashtags, including collected prepend/append blocks
|
|
63
|
-
*/
|
|
64
|
-
export interface ExpansionResult {
|
|
65
|
-
/** The inline-expanded text */
|
|
66
|
-
text: string;
|
|
67
|
-
/** Collected prepend blocks from all expanded snippets */
|
|
68
|
-
prepend: string[];
|
|
69
|
-
/** Collected append blocks from all expanded snippets */
|
|
70
|
-
append: string[];
|
|
71
|
-
}
|