opencode-snippets 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/index.d.ts +11 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +72 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/src/arg-parser.d.ts +16 -0
  6. package/dist/src/arg-parser.d.ts.map +1 -0
  7. package/dist/src/arg-parser.js +94 -0
  8. package/dist/src/arg-parser.js.map +1 -0
  9. package/dist/src/commands.d.ts +30 -0
  10. package/dist/src/commands.d.ts.map +1 -0
  11. package/dist/src/commands.js +315 -0
  12. package/dist/src/commands.js.map +1 -0
  13. package/dist/src/constants.d.ts +26 -0
  14. package/dist/src/constants.d.ts.map +1 -0
  15. package/dist/src/constants.js +28 -0
  16. package/dist/src/constants.js.map +1 -0
  17. package/dist/src/expander.d.ts +36 -0
  18. package/dist/src/expander.d.ts.map +1 -0
  19. package/dist/src/expander.js +187 -0
  20. package/dist/src/expander.js.map +1 -0
  21. package/dist/src/loader.d.ts +46 -0
  22. package/dist/src/loader.d.ts.map +1 -0
  23. package/dist/src/loader.js +223 -0
  24. package/dist/src/loader.js.map +1 -0
  25. package/dist/src/logger.d.ts +15 -0
  26. package/dist/src/logger.d.ts.map +1 -0
  27. package/dist/src/logger.js +107 -0
  28. package/dist/src/logger.js.map +1 -0
  29. package/dist/src/notification.d.ts +11 -0
  30. package/dist/src/notification.d.ts.map +1 -0
  31. package/dist/src/notification.js +26 -0
  32. package/dist/src/notification.js.map +1 -0
  33. package/dist/src/shell.d.ts +18 -0
  34. package/dist/src/shell.d.ts.map +1 -0
  35. package/dist/src/shell.js +30 -0
  36. package/dist/src/shell.js.map +1 -0
  37. package/dist/src/types.d.ts +65 -0
  38. package/dist/src/types.d.ts.map +1 -0
  39. package/dist/src/types.js +2 -0
  40. package/dist/src/types.js.map +1 -0
  41. package/package.json +8 -5
  42. package/index.ts +0 -81
  43. package/src/arg-parser.test.ts +0 -177
  44. package/src/arg-parser.ts +0 -87
  45. package/src/commands.test.ts +0 -188
  46. package/src/commands.ts +0 -414
  47. package/src/constants.ts +0 -32
  48. package/src/expander.test.ts +0 -846
  49. package/src/expander.ts +0 -225
  50. package/src/loader.test.ts +0 -352
  51. package/src/loader.ts +0 -268
  52. package/src/logger.test.ts +0 -136
  53. package/src/logger.ts +0 -121
  54. package/src/notification.ts +0 -30
  55. package/src/shell.ts +0 -50
  56. package/src/types.ts +0 -71
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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.2",
3
+ "version": "1.4.3",
4
4
  "description": "Hashtag-based snippet expansion plugin for OpenCode - instant inline text shortcuts",
5
- "main": "index.ts",
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": "tsgo --noEmit",
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": ["index.ts", "src/**/*.ts"]
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
- };
@@ -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
- }