@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,393 @@
1
+ import type { LexiconPlugin } from "../../lexicon";
2
+ import type { CompletionContext, HoverContext, CodeActionContext } from "../../lsp/types";
3
+ import { computeCapabilities } from "./capabilities";
4
+ import { toLspDiagnostics } from "./diagnostics";
5
+
6
+ /**
7
+ * JSON-RPC message types
8
+ */
9
+ interface JsonRpcRequest {
10
+ jsonrpc: "2.0";
11
+ id: string | number;
12
+ method: string;
13
+ params?: Record<string, unknown>;
14
+ }
15
+
16
+ interface JsonRpcResponse {
17
+ jsonrpc: "2.0";
18
+ id: string | number;
19
+ result?: unknown;
20
+ error?: { code: number; message: string; data?: unknown };
21
+ }
22
+
23
+ interface JsonRpcNotification {
24
+ jsonrpc: "2.0";
25
+ method: string;
26
+ params?: Record<string, unknown>;
27
+ }
28
+
29
+ /**
30
+ * stdio LSP server using Content-Length framing.
31
+ * Delegates completions, hover, and code actions to lexicon plugins.
32
+ */
33
+ export class LspServer {
34
+ private plugins: LexiconPlugin[];
35
+ openDocuments: Map<string, string> = new Map();
36
+ private buffer = "";
37
+ /** Captured notifications for testing — only populated when captureNotifications is true */
38
+ sentNotifications: Array<{ method: string; params: Record<string, unknown> }> = [];
39
+ private captureNotifications = false;
40
+
41
+ constructor(plugins: LexiconPlugin[], options?: { captureNotifications?: boolean }) {
42
+ this.plugins = plugins;
43
+ this.captureNotifications = options?.captureNotifications ?? false;
44
+ }
45
+
46
+ /**
47
+ * Start reading from stdin with Content-Length framing
48
+ */
49
+ async start(): Promise<void> {
50
+ process.stdin.setEncoding("utf-8");
51
+ process.stdin.on("data", (chunk: string) => {
52
+ this.buffer += chunk;
53
+ this.processBuffer();
54
+ });
55
+ }
56
+
57
+ /**
58
+ * Process the incoming buffer, extracting complete messages
59
+ */
60
+ private processBuffer(): void {
61
+ while (true) {
62
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
63
+ if (headerEnd === -1) break;
64
+
65
+ const header = this.buffer.slice(0, headerEnd);
66
+ const match = header.match(/Content-Length:\s*(\d+)/i);
67
+ if (!match) {
68
+ // Skip malformed header
69
+ this.buffer = this.buffer.slice(headerEnd + 4);
70
+ continue;
71
+ }
72
+
73
+ const contentLength = parseInt(match[1], 10);
74
+ const messageStart = headerEnd + 4;
75
+ const messageEnd = messageStart + contentLength;
76
+
77
+ if (this.buffer.length < messageEnd) break; // Wait for more data
78
+
79
+ const body = this.buffer.slice(messageStart, messageEnd);
80
+ this.buffer = this.buffer.slice(messageEnd);
81
+
82
+ try {
83
+ const message = JSON.parse(body);
84
+ if ("id" in message) {
85
+ this.handleRequest(message as JsonRpcRequest);
86
+ } else {
87
+ this.handleNotification(message as JsonRpcNotification);
88
+ }
89
+ } catch {
90
+ // Ignore malformed JSON
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Send a request and return the response directly (for testing).
97
+ */
98
+ async sendRequest(method: string, params?: Record<string, unknown>): Promise<JsonRpcResponse> {
99
+ try {
100
+ const result = await this.dispatch(method, params ?? {});
101
+ return { jsonrpc: "2.0", id: 0, result };
102
+ } catch (error) {
103
+ return {
104
+ jsonrpc: "2.0",
105
+ id: 0,
106
+ error: {
107
+ code: -32603,
108
+ message: error instanceof Error ? error.message : String(error),
109
+ },
110
+ };
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Handle a JSON-RPC request (has id, expects response)
116
+ */
117
+ async handleRequest(request: JsonRpcRequest): Promise<void> {
118
+ try {
119
+ const result = await this.dispatch(request.method, request.params ?? {});
120
+ this.sendResponse({ jsonrpc: "2.0", id: request.id, result });
121
+ } catch (error) {
122
+ this.sendResponse({
123
+ jsonrpc: "2.0",
124
+ id: request.id,
125
+ error: {
126
+ code: -32603,
127
+ message: error instanceof Error ? error.message : String(error),
128
+ },
129
+ });
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Handle a JSON-RPC notification (no id, no response).
135
+ * Public to allow direct testing without stdio.
136
+ */
137
+ handleNotification(notification: JsonRpcNotification): void {
138
+ switch (notification.method) {
139
+ case "initialized":
140
+ // Client acknowledged initialization
141
+ break;
142
+
143
+ case "textDocument/didOpen": {
144
+ const params = notification.params as {
145
+ textDocument: { uri: string; text: string };
146
+ };
147
+ this.openDocuments.set(params.textDocument.uri, params.textDocument.text);
148
+ this.publishDiagnostics(params.textDocument.uri, params.textDocument.text);
149
+ break;
150
+ }
151
+
152
+ case "textDocument/didChange": {
153
+ const params = notification.params as {
154
+ textDocument: { uri: string };
155
+ contentChanges: Array<{ text: string }>;
156
+ };
157
+ // Full sync: take the last content change
158
+ const text = params.contentChanges[params.contentChanges.length - 1]?.text ?? "";
159
+ this.openDocuments.set(params.textDocument.uri, text);
160
+ this.publishDiagnostics(params.textDocument.uri, text);
161
+ break;
162
+ }
163
+
164
+ case "textDocument/didClose": {
165
+ const params = notification.params as {
166
+ textDocument: { uri: string };
167
+ };
168
+ this.openDocuments.delete(params.textDocument.uri);
169
+ // Clear diagnostics for closed document
170
+ this.sendNotification("textDocument/publishDiagnostics", {
171
+ uri: params.textDocument.uri,
172
+ diagnostics: [],
173
+ });
174
+ break;
175
+ }
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Dispatch request to appropriate handler
181
+ */
182
+ private async dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {
183
+ switch (method) {
184
+ case "initialize":
185
+ return this.handleInitialize();
186
+
187
+ case "textDocument/completion":
188
+ return this.handleCompletion(params);
189
+
190
+ case "textDocument/hover":
191
+ return this.handleHover(params);
192
+
193
+ case "textDocument/codeAction":
194
+ return this.handleCodeAction(params);
195
+
196
+ case "textDocument/diagnostic":
197
+ return this.handleDiagnostic(params);
198
+
199
+ case "shutdown":
200
+ return null;
201
+
202
+ default:
203
+ return null;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Handle initialize request
209
+ */
210
+ private handleInitialize(): unknown {
211
+ return {
212
+ capabilities: computeCapabilities(this.plugins),
213
+ serverInfo: {
214
+ name: "chant",
215
+ version: "0.1.0",
216
+ },
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Handle textDocument/completion
222
+ */
223
+ private handleCompletion(params: Record<string, unknown>): unknown {
224
+ const textDocument = params.textDocument as { uri: string };
225
+ const position = params.position as { line: number; character: number };
226
+ const content = this.openDocuments.get(textDocument.uri) ?? "";
227
+
228
+ const lines = content.split("\n");
229
+ const line = lines[position.line] ?? "";
230
+ const linePrefix = line.slice(0, position.character);
231
+
232
+ // Extract word at cursor
233
+ const wordMatch = linePrefix.match(/(\w+)$/);
234
+ const wordAtCursor = wordMatch?.[1] ?? "";
235
+
236
+ const ctx: CompletionContext = {
237
+ uri: textDocument.uri,
238
+ content,
239
+ position,
240
+ wordAtCursor,
241
+ linePrefix,
242
+ };
243
+
244
+ const items = [];
245
+ for (const plugin of this.plugins) {
246
+ if (plugin.completionProvider) {
247
+ items.push(...plugin.completionProvider(ctx));
248
+ }
249
+ }
250
+
251
+ return { isIncomplete: false, items };
252
+ }
253
+
254
+ /**
255
+ * Handle textDocument/hover
256
+ */
257
+ private handleHover(params: Record<string, unknown>): unknown {
258
+ const textDocument = params.textDocument as { uri: string };
259
+ const position = params.position as { line: number; character: number };
260
+ const content = this.openDocuments.get(textDocument.uri) ?? "";
261
+
262
+ const lines = content.split("\n");
263
+ const lineText = lines[position.line] ?? "";
264
+
265
+ // Extract word at position
266
+ let start = position.character;
267
+ let end = position.character;
268
+ while (start > 0 && /\w/.test(lineText[start - 1])) start--;
269
+ while (end < lineText.length && /\w/.test(lineText[end])) end++;
270
+ const word = lineText.slice(start, end);
271
+
272
+ const ctx: HoverContext = {
273
+ uri: textDocument.uri,
274
+ content,
275
+ position,
276
+ word,
277
+ lineText,
278
+ };
279
+
280
+ // First plugin to return info wins
281
+ for (const plugin of this.plugins) {
282
+ if (plugin.hoverProvider) {
283
+ const info = plugin.hoverProvider(ctx);
284
+ if (info) {
285
+ return {
286
+ contents: { kind: "markdown", value: info.contents },
287
+ range: info.range,
288
+ };
289
+ }
290
+ }
291
+ }
292
+
293
+ return null;
294
+ }
295
+
296
+ /**
297
+ * Handle textDocument/codeAction
298
+ */
299
+ private handleCodeAction(params: Record<string, unknown>): unknown {
300
+ const textDocument = params.textDocument as { uri: string };
301
+ const range = params.range as { start: { line: number; character: number }; end: { line: number; character: number } };
302
+ const context = params.context as { diagnostics?: Array<{ code?: string; message: string; range: unknown; severity?: number }> } | undefined;
303
+ const content = this.openDocuments.get(textDocument.uri) ?? "";
304
+
305
+ const ctx: CodeActionContext = {
306
+ uri: textDocument.uri,
307
+ content,
308
+ range,
309
+ diagnostics: (context?.diagnostics ?? []).map((d) => ({
310
+ range: d.range as CodeActionContext["diagnostics"][0]["range"],
311
+ message: d.message,
312
+ ruleId: typeof d.code === "string" ? d.code : undefined,
313
+ severity: d.severity === 1 ? "error" : d.severity === 2 ? "warning" : "info",
314
+ })),
315
+ };
316
+
317
+ const actions = [];
318
+ for (const plugin of this.plugins) {
319
+ if (plugin.codeActionProvider) {
320
+ actions.push(...plugin.codeActionProvider(ctx));
321
+ }
322
+ }
323
+
324
+ return actions;
325
+ }
326
+
327
+ /**
328
+ * Handle textDocument/diagnostic (pull model)
329
+ */
330
+ private async handleDiagnostic(params: Record<string, unknown>): Promise<unknown> {
331
+ const textDocument = params.textDocument as { uri: string };
332
+ const content = this.openDocuments.get(textDocument.uri) ?? "";
333
+ const diagnostics = await this.computeDiagnostics(textDocument.uri, content);
334
+ return { kind: "full", items: diagnostics };
335
+ }
336
+
337
+ /**
338
+ * Publish diagnostics for a document (push model)
339
+ */
340
+ private async publishDiagnostics(uri: string, content: string): Promise<void> {
341
+ const diagnostics = await this.computeDiagnostics(uri, content);
342
+ this.sendNotification("textDocument/publishDiagnostics", {
343
+ uri,
344
+ diagnostics,
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Run lint engine and convert to LSP diagnostics
350
+ */
351
+ private async computeDiagnostics(uri: string, content: string): Promise<unknown[]> {
352
+ // Only lint .ts files
353
+ const filePath = uri.startsWith("file://") ? uri.slice(7) : uri;
354
+ if (!filePath.endsWith(".ts")) return [];
355
+
356
+ try {
357
+ const { runLint } = await import("../../lint/engine");
358
+ const rules = [];
359
+ for (const plugin of this.plugins) {
360
+ rules.push(...(plugin.lintRules?.() ?? []));
361
+ }
362
+
363
+ if (rules.length === 0) return [];
364
+
365
+ const diagnostics = await runLint([filePath], rules);
366
+ return toLspDiagnostics(diagnostics);
367
+ } catch {
368
+ return [];
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Send a JSON-RPC response with Content-Length framing
374
+ */
375
+ private sendResponse(response: JsonRpcResponse): void {
376
+ const body = JSON.stringify(response);
377
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
378
+ process.stdout.write(header + body);
379
+ }
380
+
381
+ /**
382
+ * Send a JSON-RPC notification with Content-Length framing
383
+ */
384
+ private sendNotification(method: string, params: Record<string, unknown>): void {
385
+ if (this.captureNotifications) {
386
+ this.sentNotifications.push({ method, params });
387
+ return;
388
+ }
389
+ const body = JSON.stringify({ jsonrpc: "2.0", method, params });
390
+ const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
391
+ process.stdout.write(header + body);
392
+ }
393
+ }
@@ -0,0 +1,257 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { parseArgs } from "./main";
3
+ import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
4
+
5
+ describe("parseArgs", () => {
6
+ test("parses command as first positional arg", () => {
7
+ const result = parseArgs(["build"]);
8
+ expect(result.command).toBe("build");
9
+ expect(result.path).toBe(".");
10
+ expect(result.help).toBe(false);
11
+ });
12
+
13
+ test("parses path as second positional arg (defaults to '.')", () => {
14
+ const result = parseArgs(["build", "./infra"]);
15
+ expect(result.command).toBe("build");
16
+ expect(result.path).toBe("./infra");
17
+ });
18
+
19
+ test("defaults path to '.' when not provided", () => {
20
+ const result = parseArgs(["build"]);
21
+ expect(result.path).toBe(".");
22
+ });
23
+
24
+ test("parses --help flag", () => {
25
+ const result = parseArgs(["--help"]);
26
+ expect(result.help).toBe(true);
27
+ });
28
+
29
+ test("parses -h flag", () => {
30
+ const result = parseArgs(["-h"]);
31
+ expect(result.help).toBe(true);
32
+ });
33
+
34
+ test("parses --output with value", () => {
35
+ const result = parseArgs(["build", "--output", "stack.json"]);
36
+ expect(result.output).toBe("stack.json");
37
+ expect(result.command).toBe("build");
38
+ });
39
+
40
+ test("parses -o with value", () => {
41
+ const result = parseArgs(["build", "-o", "stack.json"]);
42
+ expect(result.output).toBe("stack.json");
43
+ });
44
+
45
+ test("parses --format with json value", () => {
46
+ const result = parseArgs(["build", "--format", "json"]);
47
+ expect(result.format).toBe("json");
48
+ });
49
+
50
+ test("parses --format with yaml value", () => {
51
+ const result = parseArgs(["build", "--format", "yaml"]);
52
+ expect(result.format).toBe("yaml");
53
+ });
54
+
55
+ test("parses -f with json value", () => {
56
+ const result = parseArgs(["build", "-f", "json"]);
57
+ expect(result.format).toBe("json");
58
+ });
59
+
60
+ test("parses -f with yaml value", () => {
61
+ const result = parseArgs(["build", "-f", "yaml"]);
62
+ expect(result.format).toBe("yaml");
63
+ });
64
+
65
+ test("accepts any format value (validation done per-command)", () => {
66
+ const result = parseArgs(["build", "--format", "xml"]);
67
+ expect(result.format).toBe("xml"); // format is passed as-is to main
68
+ });
69
+
70
+ test("accepts invalid format values (validation done per-command)", () => {
71
+ const result = parseArgs(["build", "-f", "invalid"]);
72
+ expect(result.format).toBe("invalid"); // format is passed as-is to main
73
+ });
74
+
75
+ test("combines multiple options", () => {
76
+ const result = parseArgs([
77
+ "build",
78
+ "./infra",
79
+ "--output",
80
+ "stack.json",
81
+ "--format",
82
+ "yaml",
83
+ "--help",
84
+ ]);
85
+ expect(result.command).toBe("build");
86
+ expect(result.path).toBe("./infra");
87
+ expect(result.output).toBe("stack.json");
88
+ expect(result.format).toBe("yaml");
89
+ expect(result.help).toBe(true);
90
+ });
91
+
92
+ test("handles options in different order", () => {
93
+ const result = parseArgs([
94
+ "--output",
95
+ "stack.json",
96
+ "build",
97
+ "--format",
98
+ "yaml",
99
+ "./infra",
100
+ ]);
101
+ expect(result.command).toBe("build");
102
+ expect(result.path).toBe("./infra");
103
+ expect(result.output).toBe("stack.json");
104
+ expect(result.format).toBe("yaml");
105
+ });
106
+
107
+ test("handles empty args array", () => {
108
+ const result = parseArgs([]);
109
+ expect(result.command).toBe("");
110
+ expect(result.path).toBe(".");
111
+ expect(result.output).toBe(undefined);
112
+ expect(result.format).toBe(""); // no format specified, defaults applied per-command in main()
113
+ expect(result.help).toBe(false);
114
+ });
115
+
116
+ test("ignores unknown flags", () => {
117
+ const result = parseArgs(["build", "--unknown", "value"]);
118
+ expect(result.command).toBe("build");
119
+ // Unknown flags are silently ignored
120
+ });
121
+
122
+ test("parses --watch flag", () => {
123
+ const result = parseArgs(["build", "--watch"]);
124
+ expect(result.watch).toBe(true);
125
+ expect(result.command).toBe("build");
126
+ });
127
+
128
+ test("parses -w flag", () => {
129
+ const result = parseArgs(["build", "-w"]);
130
+ expect(result.watch).toBe(true);
131
+ });
132
+
133
+ test("watch defaults to false", () => {
134
+ const result = parseArgs(["build"]);
135
+ expect(result.watch).toBe(false);
136
+ });
137
+
138
+ test("combines --watch with other options", () => {
139
+ const result = parseArgs(["build", "./infra", "--watch", "--format", "yaml"]);
140
+ expect(result.watch).toBe(true);
141
+ expect(result.command).toBe("build");
142
+ expect(result.path).toBe("./infra");
143
+ expect(result.format).toBe("yaml");
144
+ });
145
+
146
+ test("parses --watch with lint command", () => {
147
+ const result = parseArgs(["lint", "./infra/", "-w"]);
148
+ expect(result.watch).toBe(true);
149
+ expect(result.command).toBe("lint");
150
+ expect(result.path).toBe("./infra/");
151
+ });
152
+
153
+ test("parses extraPositional as third positional arg", () => {
154
+ const result = parseArgs(["init", "lexicon", "k8s"]);
155
+ expect(result.command).toBe("init");
156
+ expect(result.path).toBe("lexicon");
157
+ expect(result.extraPositional).toBe("k8s");
158
+ });
159
+
160
+ test("parses extraPositional2 as fourth positional arg", () => {
161
+ const result = parseArgs(["init", "lexicon", "k8s", "./my-path"]);
162
+ expect(result.command).toBe("init");
163
+ expect(result.path).toBe("lexicon");
164
+ expect(result.extraPositional).toBe("k8s");
165
+ expect(result.extraPositional2).toBe("./my-path");
166
+ });
167
+
168
+ test("extraPositional2 is undefined when only 3 positional args", () => {
169
+ const result = parseArgs(["dev", "generate", "."]);
170
+ expect(result.command).toBe("dev");
171
+ expect(result.path).toBe("generate");
172
+ expect(result.extraPositional).toBe(".");
173
+ expect(result.extraPositional2).toBe(undefined);
174
+ });
175
+ });
176
+
177
+ // ── resolveCommand tests ──────────────────────────────────────────
178
+
179
+ describe("resolveCommand", () => {
180
+ const noop = async () => 0;
181
+
182
+ const testRegistry: CommandDef[] = [
183
+ { name: "build", handler: noop },
184
+ { name: "dev generate", requiresPlugins: true, handler: noop },
185
+ { name: "dev publish", requiresPlugins: true, handler: noop },
186
+ { name: "serve lsp", handler: noop },
187
+ { name: "init", handler: noop },
188
+ { name: "init lexicon", handler: noop },
189
+ { name: "dev", handler: noop },
190
+ ];
191
+
192
+ function makeArgs(overrides: Partial<ParsedArgs>): ParsedArgs {
193
+ return {
194
+ command: "",
195
+ path: ".",
196
+ format: "",
197
+ fix: false,
198
+ watch: false,
199
+ verbose: false,
200
+ help: false,
201
+ ...overrides,
202
+ };
203
+ }
204
+
205
+ test("resolves simple command", () => {
206
+ const result = resolveCommand(makeArgs({ command: "build" }), testRegistry);
207
+ expect(result).not.toBeNull();
208
+ expect(result!.def.name).toBe("build");
209
+ expect(result!.compound).toBe(false);
210
+ });
211
+
212
+ test("resolves compound command (dev generate)", () => {
213
+ const result = resolveCommand(makeArgs({ command: "dev", path: "generate" }), testRegistry);
214
+ expect(result).not.toBeNull();
215
+ expect(result!.def.name).toBe("dev generate");
216
+ expect(result!.compound).toBe(true);
217
+ });
218
+
219
+ test("resolves compound command (serve lsp)", () => {
220
+ const result = resolveCommand(makeArgs({ command: "serve", path: "lsp" }), testRegistry);
221
+ expect(result).not.toBeNull();
222
+ expect(result!.def.name).toBe("serve lsp");
223
+ expect(result!.compound).toBe(true);
224
+ });
225
+
226
+ test("resolves compound command (init lexicon)", () => {
227
+ const result = resolveCommand(makeArgs({ command: "init", path: "lexicon" }), testRegistry);
228
+ expect(result).not.toBeNull();
229
+ expect(result!.def.name).toBe("init lexicon");
230
+ expect(result!.compound).toBe(true);
231
+ });
232
+
233
+ test("falls back to simple when compound doesn't match", () => {
234
+ const result = resolveCommand(makeArgs({ command: "dev", path: "unknown" }), testRegistry);
235
+ expect(result).not.toBeNull();
236
+ expect(result!.def.name).toBe("dev");
237
+ expect(result!.compound).toBe(false);
238
+ });
239
+
240
+ test("resolves init without lexicon subcommand", () => {
241
+ const result = resolveCommand(makeArgs({ command: "init", path: "." }), testRegistry);
242
+ expect(result).not.toBeNull();
243
+ expect(result!.def.name).toBe("init");
244
+ expect(result!.compound).toBe(false);
245
+ });
246
+
247
+ test("returns null for unknown command", () => {
248
+ const result = resolveCommand(makeArgs({ command: "foobar" }), testRegistry);
249
+ expect(result).toBeNull();
250
+ });
251
+
252
+ test("compound takes priority over simple match", () => {
253
+ const result = resolveCommand(makeArgs({ command: "dev", path: "generate" }), testRegistry);
254
+ expect(result!.def.name).toBe("dev generate");
255
+ expect(result!.compound).toBe(true);
256
+ });
257
+ });