opencode-snippets 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -71
- package/index.ts +47 -17
- package/package.json +10 -5
- package/src/constants.ts +8 -8
- package/src/expander.test.ts +456 -0
- package/src/expander.ts +47 -37
- package/src/loader.test.ts +261 -0
- package/src/loader.ts +99 -45
- package/src/logger.test.ts +136 -0
- package/src/logger.ts +95 -95
- package/src/shell.ts +30 -24
- package/src/types.ts +6 -6
package/src/logger.ts
CHANGED
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { OPENCODE_CONFIG_DIR } from "./constants.js"
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { OPENCODE_CONFIG_DIR } from "./constants.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Check if debug logging is enabled via environment variable
|
|
7
7
|
*/
|
|
8
8
|
function isDebugEnabled(): boolean {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const value = process.env.DEBUG_SNIPPETS;
|
|
10
|
+
return value === "1" || value === "true";
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export class Logger {
|
|
14
|
-
|
|
14
|
+
private logDir: string;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
constructor(logDirOverride?: string) {
|
|
17
|
+
this.logDir = logDirOverride ?? join(OPENCODE_CONFIG_DIR, "logs", "snippets");
|
|
18
|
+
}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
get enabled(): boolean {
|
|
21
|
+
return isDebugEnabled();
|
|
22
|
+
}
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
24
|
+
private ensureLogDir() {
|
|
25
|
+
if (!existsSync(this.logDir)) {
|
|
26
|
+
mkdirSync(this.logDir, { recursive: true });
|
|
28
27
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
else {
|
|
49
|
-
parts.push(`${key}=${value}`)
|
|
50
|
-
}
|
|
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}`);
|
|
51
47
|
}
|
|
52
|
-
|
|
48
|
+
} else {
|
|
49
|
+
parts.push(`${key}=${value}`);
|
|
50
|
+
}
|
|
53
51
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return 'unknown'
|
|
71
|
-
} catch {
|
|
72
|
-
return 'unknown'
|
|
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";
|
|
73
68
|
}
|
|
69
|
+
}
|
|
70
|
+
return "unknown";
|
|
71
|
+
} catch {
|
|
72
|
+
return "unknown";
|
|
74
73
|
}
|
|
74
|
+
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
this.ensureLogDir()
|
|
81
|
-
|
|
82
|
-
const timestamp = new Date().toISOString()
|
|
83
|
-
const dataStr = this.formatData(data)
|
|
76
|
+
private write(level: string, component: string, message: string, data?: Record<string, unknown>) {
|
|
77
|
+
if (!this.enabled) return;
|
|
84
78
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
mkdirSync(dailyLogDir, { recursive: true })
|
|
88
|
-
}
|
|
79
|
+
try {
|
|
80
|
+
this.ensureLogDir();
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
const timestamp = new Date().toISOString();
|
|
83
|
+
const dataStr = this.formatData(data);
|
|
91
84
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
info(message: string, data?: any) {
|
|
100
|
-
const component = this.getCallerFile()
|
|
101
|
-
this.write("INFO", component, message, data)
|
|
102
|
-
}
|
|
85
|
+
const dailyLogDir = join(this.logDir, "daily");
|
|
86
|
+
if (!existsSync(dailyLogDir)) {
|
|
87
|
+
mkdirSync(dailyLogDir, { recursive: true });
|
|
88
|
+
}
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
const component = this.getCallerFile()
|
|
106
|
-
this.write("DEBUG", component, message, data)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
warn(message: string, data?: any) {
|
|
110
|
-
const component = this.getCallerFile()
|
|
111
|
-
this.write("WARN", component, message, data)
|
|
112
|
-
}
|
|
90
|
+
const logLine = `${timestamp} ${level.padEnd(5)} ${component}: ${message}${dataStr ? ` | ${dataStr}` : ""}\n`;
|
|
113
91
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
92
|
+
const logFile = join(dailyLogDir, `${new Date().toISOString().split("T")[0]}.log`);
|
|
93
|
+
writeFileSync(logFile, logLine, { flag: "a" });
|
|
94
|
+
} catch {
|
|
95
|
+
// Silent fail
|
|
117
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
118
|
}
|
|
119
119
|
|
|
120
120
|
// Export singleton logger instance
|
|
121
|
-
export const logger = new Logger()
|
|
121
|
+
export const logger = new Logger();
|
package/src/shell.ts
CHANGED
|
@@ -1,44 +1,50 @@
|
|
|
1
|
-
import { PATTERNS } from "./constants.js"
|
|
2
|
-
import { logger } from "./logger.js"
|
|
1
|
+
import { PATTERNS } from "./constants.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Executes shell commands in text using !`command` syntax
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @param text - The text containing shell commands to execute
|
|
8
8
|
* @param ctx - The plugin context (with Bun shell)
|
|
9
9
|
* @returns The text with shell commands replaced by their output
|
|
10
10
|
*/
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
|
|
17
23
|
// Reset regex state (global flag requires this)
|
|
18
|
-
PATTERNS.SHELL_COMMAND.lastIndex = 0
|
|
19
|
-
|
|
24
|
+
PATTERNS.SHELL_COMMAND.lastIndex = 0;
|
|
25
|
+
|
|
20
26
|
// Find all shell command matches
|
|
21
|
-
const matches = [...text.matchAll(PATTERNS.SHELL_COMMAND)]
|
|
22
|
-
|
|
27
|
+
const matches = [...text.matchAll(PATTERNS.SHELL_COMMAND)];
|
|
28
|
+
|
|
23
29
|
// Execute each command and replace in text
|
|
24
30
|
for (const match of matches) {
|
|
25
|
-
const cmd = match[1]
|
|
26
|
-
const
|
|
27
|
-
|
|
31
|
+
const cmd = match[1];
|
|
32
|
+
const _placeholder = match[0];
|
|
33
|
+
|
|
28
34
|
try {
|
|
29
|
-
const output = await ctx.$`${{ raw: cmd }}`.quiet().nothrow().text()
|
|
30
|
-
//
|
|
31
|
-
const replacement = `$ ${cmd}\n--> ${output.trim()}
|
|
32
|
-
result = result.replace(
|
|
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);
|
|
33
39
|
} catch (error) {
|
|
34
40
|
// If shell command fails, leave it as-is
|
|
35
41
|
// This preserves the original syntax for debugging
|
|
36
42
|
logger.warn("Shell command execution failed", {
|
|
37
43
|
command: cmd,
|
|
38
|
-
error: error instanceof Error ? error.message : String(error)
|
|
39
|
-
})
|
|
44
|
+
error: error instanceof Error ? error.message : String(error),
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
|
-
|
|
43
|
-
return result
|
|
48
|
+
|
|
49
|
+
return result;
|
|
44
50
|
}
|
package/src/types.ts
CHANGED
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export interface Snippet {
|
|
5
5
|
/** The primary name/key of the snippet */
|
|
6
|
-
name: string
|
|
6
|
+
name: string;
|
|
7
7
|
/** The content of the snippet (without frontmatter) */
|
|
8
|
-
content: string
|
|
8
|
+
content: string;
|
|
9
9
|
/** Alternative names that also trigger this snippet */
|
|
10
|
-
aliases: string[]
|
|
10
|
+
aliases: string[];
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Snippet registry that maps keys to content
|
|
15
15
|
*/
|
|
16
|
-
export type SnippetRegistry = Map<string, string
|
|
16
|
+
export type SnippetRegistry = Map<string, string>;
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Frontmatter data from snippet files
|
|
20
20
|
*/
|
|
21
21
|
export interface SnippetFrontmatter {
|
|
22
22
|
/** Alternative hashtags for this snippet */
|
|
23
|
-
aliases?: string[]
|
|
23
|
+
aliases?: string[];
|
|
24
24
|
/** Optional description of what this snippet does */
|
|
25
|
-
description?: string
|
|
25
|
+
description?: string;
|
|
26
26
|
}
|