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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OpencodeClient } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sends a message that will be displayed but ignored by the AI
|
|
4
|
+
* Used for command output that shouldn't trigger AI responses
|
|
5
|
+
*
|
|
6
|
+
* @param client - The OpenCode client instance
|
|
7
|
+
* @param sessionId - The current session ID
|
|
8
|
+
* @param text - The text to display
|
|
9
|
+
*/
|
|
10
|
+
export declare function sendIgnoredMessage(client: OpencodeClient, sessionId: string, text: string): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=notification.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification.d.ts","sourceRoot":"","sources":["../../src/notification.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,cAAc,EACtB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAcf"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
/**
|
|
3
|
+
* Sends a message that will be displayed but ignored by the AI
|
|
4
|
+
* Used for command output that shouldn't trigger AI responses
|
|
5
|
+
*
|
|
6
|
+
* @param client - The OpenCode client instance
|
|
7
|
+
* @param sessionId - The current session ID
|
|
8
|
+
* @param text - The text to display
|
|
9
|
+
*/
|
|
10
|
+
export async function sendIgnoredMessage(client, sessionId, text) {
|
|
11
|
+
try {
|
|
12
|
+
await client.session.prompt({
|
|
13
|
+
path: { id: sessionId },
|
|
14
|
+
body: {
|
|
15
|
+
noReply: true,
|
|
16
|
+
parts: [{ type: "text", text, ignored: true }],
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
logger.error("Failed to send ignored message", {
|
|
22
|
+
error: error instanceof Error ? error.message : String(error),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=notification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notification.js","sourceRoot":"","sources":["../../src/notification.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAsB,EACtB,SAAiB,EACjB,IAAY;IAEZ,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAC1B,IAAI,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE;YACvB,IAAI,EAAE;gBACJ,OAAO,EAAE,IAAI;gBACb,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;aAC/C;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,KAAK,CAAC,gCAAgC,EAAE;YAC7C,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;SAC9D,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes shell commands in text using !`command` syntax
|
|
3
|
+
*
|
|
4
|
+
* @param text - The text containing shell commands to execute
|
|
5
|
+
* @param ctx - The plugin context (with Bun shell)
|
|
6
|
+
* @returns The text with shell commands replaced by their output
|
|
7
|
+
*/
|
|
8
|
+
export type ShellContext = {
|
|
9
|
+
$: (template: TemplateStringsArray, ...args: unknown[]) => {
|
|
10
|
+
quiet: () => {
|
|
11
|
+
nothrow: () => {
|
|
12
|
+
text: () => Promise<string>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export declare function executeShellCommands(text: string, ctx: ShellContext): Promise<string>;
|
|
18
|
+
//# sourceMappingURL=shell.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../../src/shell.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,CAAC,EAAE,CACD,QAAQ,EAAE,oBAAoB,EAC9B,GAAG,IAAI,EAAE,OAAO,EAAE,KACf;QACH,KAAK,EAAE,MAAM;YAAE,OAAO,EAAE,MAAM;gBAAE,IAAI,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAA;aAAE,CAAA;SAAE,CAAC;KACjE,CAAC;CACH,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B3F"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PATTERNS } from "./constants.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
3
|
+
export async function executeShellCommands(text, ctx) {
|
|
4
|
+
let result = text;
|
|
5
|
+
// Reset regex state (global flag requires this)
|
|
6
|
+
PATTERNS.SHELL_COMMAND.lastIndex = 0;
|
|
7
|
+
// Find all shell command matches
|
|
8
|
+
const matches = [...text.matchAll(PATTERNS.SHELL_COMMAND)];
|
|
9
|
+
// Execute each command and replace in text
|
|
10
|
+
for (const match of matches) {
|
|
11
|
+
const cmd = match[1];
|
|
12
|
+
const _placeholder = match[0];
|
|
13
|
+
try {
|
|
14
|
+
const output = await ctx.$ `${{ raw: cmd }}`.quiet().nothrow().text();
|
|
15
|
+
// Deviate from slash commands' substitution mechanism: print command first, then output
|
|
16
|
+
const replacement = `$ ${cmd}\n--> ${output.trim()}`;
|
|
17
|
+
result = result.replace(_placeholder, replacement);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// If shell command fails, leave it as-is
|
|
21
|
+
// This preserves the original syntax for debugging
|
|
22
|
+
logger.warn("Shell command execution failed", {
|
|
23
|
+
command: cmd,
|
|
24
|
+
error: error instanceof Error ? error.message : String(error),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=shell.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAkBrC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,IAAY,EAAE,GAAiB;IACxE,IAAI,MAAM,GAAG,IAAI,CAAC;IAElB,gDAAgD;IAChD,QAAQ,CAAC,aAAa,CAAC,SAAS,GAAG,CAAC,CAAC;IAErC,iCAAiC;IACjC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,CAAC;IAE3D,2CAA2C;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAE9B,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,CAAC,CAAA,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC;YACrE,wFAAwF;YACxF,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;YACrD,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;QACrD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,yCAAyC;YACzC,mDAAmD;YACnD,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE;gBAC5C,OAAO,EAAE,GAAG;gBACZ,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAC9D,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* OpenCode client type from the SDK
|
|
4
|
+
*/
|
|
5
|
+
export type OpencodeClient = ReturnType<typeof createOpencodeClient>;
|
|
6
|
+
/**
|
|
7
|
+
* A snippet with its content and metadata
|
|
8
|
+
*/
|
|
9
|
+
export interface Snippet {
|
|
10
|
+
/** The primary name/key of the snippet */
|
|
11
|
+
name: string;
|
|
12
|
+
/** The content of the snippet (without frontmatter) */
|
|
13
|
+
content: string;
|
|
14
|
+
/** Alternative names that also trigger this snippet */
|
|
15
|
+
aliases: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extended snippet info with file metadata
|
|
19
|
+
*/
|
|
20
|
+
export interface SnippetInfo {
|
|
21
|
+
name: string;
|
|
22
|
+
content: string;
|
|
23
|
+
aliases: string[];
|
|
24
|
+
description?: string;
|
|
25
|
+
filePath: string;
|
|
26
|
+
source: "global" | "project";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Snippet registry that maps keys to snippet info
|
|
30
|
+
*/
|
|
31
|
+
export type SnippetRegistry = Map<string, SnippetInfo>;
|
|
32
|
+
/**
|
|
33
|
+
* Frontmatter data from snippet files
|
|
34
|
+
*/
|
|
35
|
+
export interface SnippetFrontmatter {
|
|
36
|
+
/** Alternative hashtags for this snippet (plural form, preferred) */
|
|
37
|
+
aliases?: string | string[];
|
|
38
|
+
/** Alternative hashtags for this snippet (singular form, also accepted) */
|
|
39
|
+
alias?: string | string[];
|
|
40
|
+
/** Optional description of what this snippet does */
|
|
41
|
+
description?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Parsed snippet content with inline text and prepend/append blocks
|
|
45
|
+
*/
|
|
46
|
+
export interface ParsedSnippetContent {
|
|
47
|
+
/** Content outside blocks (replaces hashtag inline) */
|
|
48
|
+
inline: string;
|
|
49
|
+
/** <prepend> block contents in document order */
|
|
50
|
+
prepend: string[];
|
|
51
|
+
/** <append> block contents in document order */
|
|
52
|
+
append: string[];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Result of expanding hashtags, including collected prepend/append blocks
|
|
56
|
+
*/
|
|
57
|
+
export interface ExpansionResult {
|
|
58
|
+
/** The inline-expanded text */
|
|
59
|
+
text: string;
|
|
60
|
+
/** Collected prepend blocks from all expanded snippets */
|
|
61
|
+
prepend: string[];
|
|
62
|
+
/** Collected append blocks from all expanded snippets */
|
|
63
|
+
append: string[];
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAErE;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gDAAgD;IAChD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yDAAyD;IACzD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-snippets",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts",
|
|
5
|
-
"main": "index.
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
6
7
|
"type": "module",
|
|
7
8
|
"engines": {
|
|
8
9
|
"bun": ">=1.0.0"
|
|
@@ -42,11 +43,13 @@
|
|
|
42
43
|
"typescript": "^5"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
|
-
"build": "
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsgo --noEmit",
|
|
46
48
|
"format:check": "biome check .",
|
|
47
49
|
"format:fix": "biome check --write .",
|
|
48
50
|
"ai:check": "bun run format:fix",
|
|
49
|
-
"prepare": "husky"
|
|
51
|
+
"prepare": "husky",
|
|
52
|
+
"prepublishOnly": "bun run build"
|
|
50
53
|
},
|
|
51
|
-
"files": ["
|
|
54
|
+
"files": ["dist"]
|
|
52
55
|
}
|
package/index.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
-
import { createCommandExecuteHandler } from "./src/commands.js";
|
|
3
|
-
import { assembleMessage, expandHashtags } from "./src/expander.js";
|
|
4
|
-
import { loadSnippets } from "./src/loader.js";
|
|
5
|
-
import { logger } from "./src/logger.js";
|
|
6
|
-
import { executeShellCommands, type ShellContext } from "./src/shell.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Snippets Plugin for OpenCode
|
|
10
|
-
*
|
|
11
|
-
* Expands hashtag-based shortcuts in user messages into predefined text snippets.
|
|
12
|
-
* Also provides /snippet command for managing snippets.
|
|
13
|
-
*
|
|
14
|
-
* @see https://github.com/JosXa/opencode-snippets for full documentation
|
|
15
|
-
*/
|
|
16
|
-
export const SnippetsPlugin: Plugin = async (ctx) => {
|
|
17
|
-
// Load all snippets at startup (global + project directory)
|
|
18
|
-
const startupStart = performance.now();
|
|
19
|
-
const snippets = await loadSnippets(ctx.directory);
|
|
20
|
-
const startupTime = performance.now() - startupStart;
|
|
21
|
-
|
|
22
|
-
logger.debug("Plugin startup complete", {
|
|
23
|
-
startupTimeMs: startupTime.toFixed(2),
|
|
24
|
-
snippetCount: snippets.size,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
// Create command handler
|
|
28
|
-
const commandHandler = createCommandExecuteHandler(ctx.client, snippets, ctx.directory);
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
// Register /snippet command
|
|
32
|
-
config: async (opencodeConfig) => {
|
|
33
|
-
opencodeConfig.command ??= {};
|
|
34
|
-
opencodeConfig.command.snippet = {
|
|
35
|
-
template: "",
|
|
36
|
-
description: "Manage text snippets (add, delete, list, help)",
|
|
37
|
-
};
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
// Handle /snippet command execution
|
|
41
|
-
"command.execute.before": commandHandler,
|
|
42
|
-
|
|
43
|
-
"chat.message": async (_input, output) => {
|
|
44
|
-
// Only process user messages, never assistant messages
|
|
45
|
-
if (output.message.role !== "user") return;
|
|
46
|
-
|
|
47
|
-
const messageStart = performance.now();
|
|
48
|
-
let expandTimeTotal = 0;
|
|
49
|
-
let shellTimeTotal = 0;
|
|
50
|
-
let processedParts = 0;
|
|
51
|
-
|
|
52
|
-
for (const part of output.parts) {
|
|
53
|
-
if (part.type === "text" && part.text) {
|
|
54
|
-
// 1. Expand hashtags recursively with loop detection
|
|
55
|
-
const expandStart = performance.now();
|
|
56
|
-
const expansionResult = expandHashtags(part.text, snippets);
|
|
57
|
-
part.text = assembleMessage(expansionResult);
|
|
58
|
-
const expandTime = performance.now() - expandStart;
|
|
59
|
-
expandTimeTotal += expandTime;
|
|
60
|
-
|
|
61
|
-
// 2. Execute shell commands: !`command`
|
|
62
|
-
const shellStart = performance.now();
|
|
63
|
-
part.text = await executeShellCommands(part.text, ctx as unknown as ShellContext);
|
|
64
|
-
const shellTime = performance.now() - shellStart;
|
|
65
|
-
shellTimeTotal += shellTime;
|
|
66
|
-
processedParts += 1;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const totalTime = performance.now() - messageStart;
|
|
71
|
-
if (processedParts > 0) {
|
|
72
|
-
logger.debug("Message processing complete", {
|
|
73
|
-
totalTimeMs: totalTime.toFixed(2),
|
|
74
|
-
snippetExpandTimeMs: expandTimeTotal.toFixed(2),
|
|
75
|
-
shellTimeMs: shellTimeTotal.toFixed(2),
|
|
76
|
-
processedParts,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
};
|
|
81
|
-
};
|
package/src/arg-parser.test.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { parseCommandArgs } from "./arg-parser.js";
|
|
3
|
-
|
|
4
|
-
describe("parseCommandArgs", () => {
|
|
5
|
-
// Basic splitting
|
|
6
|
-
describe("basic argument splitting", () => {
|
|
7
|
-
it("splits simple space-separated args", () => {
|
|
8
|
-
expect(parseCommandArgs("add test")).toEqual(["add", "test"]);
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("handles empty input", () => {
|
|
12
|
-
expect(parseCommandArgs("")).toEqual([]);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("handles whitespace-only input", () => {
|
|
16
|
-
expect(parseCommandArgs(" ")).toEqual([]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("handles multiple spaces between args", () => {
|
|
20
|
-
expect(parseCommandArgs("add test")).toEqual(["add", "test"]);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("handles leading and trailing spaces", () => {
|
|
24
|
-
expect(parseCommandArgs(" add test ")).toEqual(["add", "test"]);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("handles tabs and mixed whitespace", () => {
|
|
28
|
-
expect(parseCommandArgs("add\t\ttest")).toEqual(["add", "test"]);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Double quote handling
|
|
33
|
-
describe("double quote handling", () => {
|
|
34
|
-
it("preserves double-quoted strings with spaces", () => {
|
|
35
|
-
expect(parseCommandArgs('add "hello world"')).toEqual(["add", "hello world"]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("handles single quote inside double quotes", () => {
|
|
39
|
-
// THE MAIN BUG - apostrophe in description
|
|
40
|
-
expect(parseCommandArgs('--desc="don\'t do this"')).toEqual(["--desc=don't do this"]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("handles empty double-quoted string", () => {
|
|
44
|
-
expect(parseCommandArgs('add ""')).toEqual(["add", ""]);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("handles double-quoted string at start", () => {
|
|
48
|
-
expect(parseCommandArgs('"hello world" test')).toEqual(["hello world", "test"]);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("handles multiple double-quoted strings", () => {
|
|
52
|
-
expect(parseCommandArgs('"first" "second"')).toEqual(["first", "second"]);
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Single quote handling
|
|
57
|
-
describe("single quote handling", () => {
|
|
58
|
-
it("preserves single-quoted strings with spaces", () => {
|
|
59
|
-
expect(parseCommandArgs("add 'hello world'")).toEqual(["add", "hello world"]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("handles double quote inside single quotes", () => {
|
|
63
|
-
expect(parseCommandArgs("--desc='say \"hello\"'")).toEqual(['--desc=say "hello"']);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("handles empty single-quoted string", () => {
|
|
67
|
-
expect(parseCommandArgs("add ''")).toEqual(["add", ""]);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// --key=value syntax
|
|
72
|
-
describe("--key=value syntax", () => {
|
|
73
|
-
it("handles --key=value without quotes", () => {
|
|
74
|
-
expect(parseCommandArgs("--desc=hello")).toEqual(["--desc=hello"]);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('handles --key="value" with quotes stripped from value', () => {
|
|
78
|
-
expect(parseCommandArgs('--desc="hello world"')).toEqual(["--desc=hello world"]);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("handles --key='value' with quotes stripped from value", () => {
|
|
82
|
-
expect(parseCommandArgs("--key='hello world'")).toEqual(["--key=hello world"]);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("handles --key=value with special characters", () => {
|
|
86
|
-
expect(parseCommandArgs("--desc=hello,world")).toEqual(["--desc=hello,world"]);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("preserves = inside quoted value", () => {
|
|
90
|
-
expect(parseCommandArgs('--desc="a=b"')).toEqual(["--desc=a=b"]);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Multiline content (critical for snippet bodies)
|
|
95
|
-
describe("multiline content", () => {
|
|
96
|
-
it("handles multiline content in double quotes", () => {
|
|
97
|
-
expect(parseCommandArgs('add test "line1\nline2\nline3"')).toEqual([
|
|
98
|
-
"add",
|
|
99
|
-
"test",
|
|
100
|
-
"line1\nline2\nline3",
|
|
101
|
-
]);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("handles multiline content in single quotes", () => {
|
|
105
|
-
expect(parseCommandArgs("add test 'line1\nline2'")).toEqual(["add", "test", "line1\nline2"]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("handles multiline content with --key=value syntax", () => {
|
|
109
|
-
expect(parseCommandArgs('--desc="line1\nline2"')).toEqual(["--desc=line1\nline2"]);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it("preserves indentation in multiline content", () => {
|
|
113
|
-
const input = 'add test "line1\n indented\n more indented"';
|
|
114
|
-
expect(parseCommandArgs(input)).toEqual([
|
|
115
|
-
"add",
|
|
116
|
-
"test",
|
|
117
|
-
"line1\n indented\n more indented",
|
|
118
|
-
]);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// Mixed scenarios
|
|
123
|
-
describe("mixed scenarios", () => {
|
|
124
|
-
it("handles mixed quoted and unquoted args", () => {
|
|
125
|
-
expect(parseCommandArgs('add test "hello world" --project')).toEqual([
|
|
126
|
-
"add",
|
|
127
|
-
"test",
|
|
128
|
-
"hello world",
|
|
129
|
-
"--project",
|
|
130
|
-
]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("handles complex command with all option types", () => {
|
|
134
|
-
const input = 'add mysnippet "content here" --aliases=a,b --desc="don\'t forget" --project';
|
|
135
|
-
expect(parseCommandArgs(input)).toEqual([
|
|
136
|
-
"add",
|
|
137
|
-
"mysnippet",
|
|
138
|
-
"content here",
|
|
139
|
-
"--aliases=a,b",
|
|
140
|
-
"--desc=don't forget",
|
|
141
|
-
"--project",
|
|
142
|
-
]);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("handles --key value syntax (space-separated)", () => {
|
|
146
|
-
expect(parseCommandArgs("--desc hello")).toEqual(["--desc", "hello"]);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("handles --key followed by quoted value", () => {
|
|
150
|
-
expect(parseCommandArgs('--desc "hello world"')).toEqual(["--desc", "hello world"]);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// Edge cases
|
|
155
|
-
describe("edge cases", () => {
|
|
156
|
-
it("handles unclosed double quote by including rest of string", () => {
|
|
157
|
-
// Graceful handling: treat unclosed quote as extending to end
|
|
158
|
-
expect(parseCommandArgs('add "unclosed')).toEqual(["add", "unclosed"]);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("handles unclosed single quote by including rest of string", () => {
|
|
162
|
-
expect(parseCommandArgs("add 'unclosed")).toEqual(["add", "unclosed"]);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("handles backslash-escaped quotes inside double quotes", () => {
|
|
166
|
-
expect(parseCommandArgs('--desc="say \\"hello\\""')).toEqual(['--desc=say "hello"']);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("handles backslash-escaped quotes inside single quotes", () => {
|
|
170
|
-
expect(parseCommandArgs("--desc='it\\'s fine'")).toEqual(["--desc=it's fine"]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("handles literal backslash", () => {
|
|
174
|
-
expect(parseCommandArgs('--path="C:\\\\Users"')).toEqual(["--path=C:\\Users"]);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
});
|
package/src/arg-parser.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell-like argument parser that handles quoted strings correctly.
|
|
3
|
-
*
|
|
4
|
-
* Supports:
|
|
5
|
-
* - Space-separated arguments
|
|
6
|
-
* - Double-quoted strings (preserves spaces, allows single quotes inside)
|
|
7
|
-
* - Single-quoted strings (preserves spaces, allows double quotes inside)
|
|
8
|
-
* - --key=value syntax with quoted values
|
|
9
|
-
* - Multiline content inside quotes
|
|
10
|
-
* - Backslash escapes for quotes inside quoted strings
|
|
11
|
-
*
|
|
12
|
-
* @param input - The raw argument string to parse
|
|
13
|
-
* @returns Array of parsed arguments with quotes stripped
|
|
14
|
-
*/
|
|
15
|
-
export function parseCommandArgs(input: string): string[] {
|
|
16
|
-
const args: string[] = [];
|
|
17
|
-
let current = "";
|
|
18
|
-
let state: "normal" | "double" | "single" = "normal";
|
|
19
|
-
let hasQuotedContent = false; // Track if we've entered a quoted section
|
|
20
|
-
let i = 0;
|
|
21
|
-
|
|
22
|
-
while (i < input.length) {
|
|
23
|
-
const char = input[i];
|
|
24
|
-
|
|
25
|
-
if (state === "normal") {
|
|
26
|
-
if (char === " " || char === "\t") {
|
|
27
|
-
// Whitespace in normal mode: finish current token
|
|
28
|
-
if (current.length > 0 || hasQuotedContent) {
|
|
29
|
-
args.push(current);
|
|
30
|
-
current = "";
|
|
31
|
-
hasQuotedContent = false;
|
|
32
|
-
}
|
|
33
|
-
} else if (char === '"') {
|
|
34
|
-
// Enter double-quote mode
|
|
35
|
-
state = "double";
|
|
36
|
-
hasQuotedContent = true;
|
|
37
|
-
} else if (char === "'") {
|
|
38
|
-
// Enter single-quote mode
|
|
39
|
-
state = "single";
|
|
40
|
-
hasQuotedContent = true;
|
|
41
|
-
} else {
|
|
42
|
-
current += char;
|
|
43
|
-
}
|
|
44
|
-
} else if (state === "double") {
|
|
45
|
-
if (char === "\\") {
|
|
46
|
-
// Check for escape sequences
|
|
47
|
-
const next = input[i + 1];
|
|
48
|
-
if (next === '"' || next === "\\") {
|
|
49
|
-
current += next;
|
|
50
|
-
i++; // Skip the escaped character
|
|
51
|
-
} else {
|
|
52
|
-
current += char;
|
|
53
|
-
}
|
|
54
|
-
} else if (char === '"') {
|
|
55
|
-
// Exit double-quote mode
|
|
56
|
-
state = "normal";
|
|
57
|
-
} else {
|
|
58
|
-
current += char;
|
|
59
|
-
}
|
|
60
|
-
} else if (state === "single") {
|
|
61
|
-
if (char === "\\") {
|
|
62
|
-
// Check for escape sequences
|
|
63
|
-
const next = input[i + 1];
|
|
64
|
-
if (next === "'" || next === "\\") {
|
|
65
|
-
current += next;
|
|
66
|
-
i++; // Skip the escaped character
|
|
67
|
-
} else {
|
|
68
|
-
current += char;
|
|
69
|
-
}
|
|
70
|
-
} else if (char === "'") {
|
|
71
|
-
// Exit single-quote mode
|
|
72
|
-
state = "normal";
|
|
73
|
-
} else {
|
|
74
|
-
current += char;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
i++;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Handle any remaining content (including unclosed quotes or empty quoted strings)
|
|
82
|
-
if (current.length > 0 || hasQuotedContent) {
|
|
83
|
-
args.push(current);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return args;
|
|
87
|
-
}
|