headlesscode 1.0.2

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 (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. package/tsconfig.json +18 -0
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Shared TypeScript program loading for the code-intelligence tools
3
+ * (`outline`, `go_to_definition`, `find_references`, `import_graph`).
4
+ *
5
+ * All four tools need the same foundational capability — parse the target
6
+ * workspace's real TypeScript source respecting its actual `tsconfig.json`
7
+ * (including `paths` aliases like this repo's `vscode` → shim mapping) — so
8
+ * they share ONE module that owns program loading, instead of four
9
+ * independent implementations drifting into four subtly different analyses.
10
+ *
11
+ * Loading is deliberately built on the real Compiler API, never a regex or a
12
+ * hand-rolled parser:
13
+ * - `ts.findConfigFile` + `ts.readConfigFile` +
14
+ * `ts.parseJsonConfigFileContent` for tsconfig handling (not hand-rolled)
15
+ * - `ts.createProgram` for the type-checked program
16
+ * - `ts.createLanguageService` (backed by the program) for definition
17
+ * resolution (`getDefinitionAtPosition`) and reference finding
18
+ * (`getReferencesAtPosition`)
19
+ *
20
+ * Caching: rebuilding a full TS program is expensive, so a loaded program is
21
+ * cached per workspace root for a session's lifetime and only rebuilt when a
22
+ * file the program covers has actually changed. We track the mtimes of the
23
+ * project's own source files (the tsconfig's resolved `fileNames`, i.e. the
24
+ * files an edit tool can realistically touch) plus the tsconfig file itself;
25
+ * node_modules types are deliberately NOT tracked (they don't change
26
+ * mid-session). The model edits a `.ts` file in the workspace → its mtime
27
+ * changes → the next tool call rebuilds. `CodeIntelCache.programLoadCount`
28
+ * exists so tests can prove the cache is reused with a real call counter,
29
+ * mirroring how caching behavior is proven elsewhere in this project.
30
+ */
31
+
32
+ import * as fs from "node:fs"
33
+ import * as path from "node:path"
34
+ import * as ts from "typescript"
35
+
36
+ /** Everything a code-intelligence tool call needs from a loaded workspace. */
37
+ export interface CodeIntel {
38
+ program: ts.Program
39
+ languageService: ts.LanguageService
40
+ compilerOptions: ts.CompilerOptions
41
+ /** Absolute path of the tsconfig that produced this program (undefined if none). */
42
+ configFilePath: string | undefined
43
+ /** Absolute paths of the project's own source files (tsconfig `fileNames`). */
44
+ projectFiles: string[]
45
+ /**
46
+ * Ad-hoc files served by the language-service host that are NOT part of
47
+ * the program (e.g. a `.ts` file the model just created with write_to_file
48
+ * after the program was last loaded). They parse on demand against the
49
+ * program's compiler options; cross-file resolution still goes through the
50
+ * program's checker.
51
+ */
52
+ includeExtraFile(fileName: string): void
53
+ }
54
+
55
+ /** A per-workspace cache of a loaded program + language service. */
56
+ export class CodeIntelCache {
57
+ /** Real load counter: 1 per actual program build (tests assert on this). */
58
+ programLoadCount = 0
59
+
60
+ private loaded: { intel: CodeIntel; trackedMtimes: Map<string, number> } | undefined
61
+
62
+ constructor(readonly workspaceRoot: string) {}
63
+
64
+ /** Load-or-reuse the cached program, rebuilding only if a covered file changed. */
65
+ get(): CodeIntel {
66
+ if (this.loaded !== undefined && !this.hasCoveredFileChanged()) {
67
+ return this.loaded.intel
68
+ }
69
+ const intel = loadCodeIntel(this.workspaceRoot)
70
+ this.loaded = { intel, trackedMtimes: trackMtimes(intel) }
71
+ this.programLoadCount += 1
72
+ return intel
73
+ }
74
+
75
+ /** Drop the cached program (used by tests; not needed by session code). */
76
+ invalidate(): void {
77
+ this.loaded = undefined
78
+ }
79
+
80
+ private hasCoveredFileChanged(): boolean {
81
+ for (const [file, mtime] of this.loaded!.trackedMtimes) {
82
+ try {
83
+ if (fs.statSync(file).mtimeMs !== mtime) {
84
+ return true
85
+ }
86
+ } catch {
87
+ // File deleted since load — definitely stale.
88
+ return true
89
+ }
90
+ }
91
+ return false
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Per-workspace caches, keyed by the (resolved) workspace root. One process
97
+ * can host several workspaces (headless session + reviewer/QA executors), and
98
+ * each keeps its own program — same pattern as the browser sessions registry.
99
+ */
100
+ const caches = new Map<string, CodeIntelCache>()
101
+
102
+ /** Get (creating if needed) the program cache for a workspace root. */
103
+ export function getCodeIntelCache(workspaceRoot: string): CodeIntelCache {
104
+ const root = path.resolve(workspaceRoot)
105
+ let cache = caches.get(root)
106
+ if (cache === undefined) {
107
+ cache = new CodeIntelCache(root)
108
+ caches.set(root, cache)
109
+ }
110
+ return cache
111
+ }
112
+
113
+ /** Drop every cached program (test isolation). */
114
+ export function resetCodeIntelCaches(): void {
115
+ caches.clear()
116
+ }
117
+
118
+ /** Record the mtimes of the files whose change invalidates the program. */
119
+ function trackMtimes(intel: CodeIntel): Map<string, number> {
120
+ const mtimes = new Map<string, number>()
121
+ for (const file of intel.projectFiles) {
122
+ try {
123
+ mtimes.set(file, fs.statSync(file).mtimeMs)
124
+ } catch {
125
+ // File vanished between program load and tracking — leave untracked;
126
+ // the next get() rebuilds only when a TRACKED file changes. A deleted
127
+ // untracked file is still served from the cached program snapshot.
128
+ }
129
+ }
130
+ if (intel.configFilePath !== undefined) {
131
+ try {
132
+ mtimes.set(intel.configFilePath, fs.statSync(intel.configFilePath).mtimeMs)
133
+ } catch {
134
+ // Same as above.
135
+ }
136
+ }
137
+ return mtimes
138
+ }
139
+
140
+ function loadCodeIntel(workspaceRoot: string): CodeIntel {
141
+ const configFilePath = ts.findConfigFile(workspaceRoot, ts.sys.fileExists, "tsconfig.json")
142
+
143
+ let compilerOptions: ts.CompilerOptions
144
+ let rootNames: string[]
145
+ let configFileParsingDiagnostics: ts.Diagnostic[] = []
146
+
147
+ if (configFilePath !== undefined) {
148
+ const read = ts.readConfigFile(configFilePath, ts.sys.readFile)
149
+ if (read.error !== undefined) {
150
+ throw new Error(
151
+ `cannot read tsconfig at ${configFilePath}: ${ts.flattenDiagnosticMessageText(read.error.messageText, "\n")}`,
152
+ )
153
+ }
154
+ const parsed = ts.parseJsonConfigFileContent(
155
+ read.config,
156
+ {
157
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
158
+ readDirectory: ts.sys.readDirectory,
159
+ fileExists: ts.sys.fileExists,
160
+ readFile: ts.sys.readFile,
161
+ },
162
+ path.dirname(configFilePath),
163
+ undefined,
164
+ configFilePath,
165
+ )
166
+ compilerOptions = parsed.options
167
+ rootNames = parsed.fileNames
168
+ configFileParsingDiagnostics = parsed.errors
169
+ } else {
170
+ // No tsconfig: fall back to a sensible default program over every
171
+ // TypeScript file in the workspace (excluding deps/vcs/hidden dirs).
172
+ // This keeps the tools working on scratch workspaces and fixtures
173
+ // without requiring the target to have a tsconfig of its own.
174
+ compilerOptions = {
175
+ target: ts.ScriptTarget.ES2022,
176
+ module: ts.ModuleKind.ESNext,
177
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
178
+ strict: false,
179
+ skipLibCheck: true,
180
+ }
181
+ rootNames = discoverTypeScriptFiles(workspaceRoot)
182
+ }
183
+
184
+ const program = ts.createProgram({
185
+ rootNames,
186
+ options: compilerOptions,
187
+ configFileParsingDiagnostics,
188
+ })
189
+
190
+ const host = new LsHost(program, compilerOptions)
191
+ const languageService = ts.createLanguageService(host)
192
+
193
+ return {
194
+ program,
195
+ languageService,
196
+ compilerOptions,
197
+ configFilePath: configFilePath ?? undefined,
198
+ projectFiles: rootNames,
199
+ includeExtraFile: (fileName) => host.includeExtraFile(fileName),
200
+ }
201
+ }
202
+
203
+ /** Recursively find every `.ts`/`.tsx`/`.mts`/`.cts` file, skipping noise dirs. */
204
+ function discoverTypeScriptFiles(root: string): string[] {
205
+ const found: string[] = []
206
+ const skip = new Set(["node_modules", ".git", ".hg", ".svn", "dist", "out", "build", "coverage"])
207
+ const walk = (dir: string): void => {
208
+ let entries: fs.Dirent[]
209
+ try {
210
+ entries = fs.readdirSync(dir, { withFileTypes: true })
211
+ } catch {
212
+ return
213
+ }
214
+ for (const entry of entries) {
215
+ if (entry.name.startsWith(".") || skip.has(entry.name)) {
216
+ continue
217
+ }
218
+ const full = path.join(dir, entry.name)
219
+ if (entry.isDirectory()) {
220
+ walk(full)
221
+ } else if (isSupportedSource(entry.name)) {
222
+ found.push(full)
223
+ }
224
+ }
225
+ }
226
+ walk(root)
227
+ return found
228
+ }
229
+
230
+ /** True for file names the TS compiler can parse as source. */
231
+ export function isSupportedSource(fileName: string): boolean {
232
+ return /\.(m|c)?(t|j)sx?$/i.test(fileName) || /\.d\.(m|c)?ts$/i.test(fileName)
233
+ }
234
+
235
+ /**
236
+ * Map a file name to the TS ScriptKind by extension. The compiler's internal
237
+ * `getScriptKindFromFileName` exists at runtime but is NOT part of the public
238
+ * d.ts, so we keep a small local equivalent rather than casting to an untyped
239
+ * API.
240
+ */
241
+ export function scriptKindFromFileName(fileName: string): ts.ScriptKind {
242
+ const lower = fileName.toLowerCase()
243
+ if (lower.endsWith(".tsx")) {
244
+ return ts.ScriptKind.TSX
245
+ }
246
+ if (lower.endsWith(".jsx")) {
247
+ return ts.ScriptKind.JSX
248
+ }
249
+ if (lower.endsWith(".mts") || lower.endsWith(".cts") || lower.endsWith(".ts")) {
250
+ return ts.ScriptKind.TS
251
+ }
252
+ if (lower.endsWith(".mjs") || lower.endsWith(".cjs") || lower.endsWith(".js")) {
253
+ return ts.ScriptKind.JS
254
+ }
255
+ if (lower.endsWith(".json")) {
256
+ return ts.ScriptKind.JSON
257
+ }
258
+ return ts.ScriptKind.Unknown
259
+ }
260
+
261
+ /**
262
+ * Language-service host backed by the cached program's already-parsed source
263
+ * files (no re-reading from disk for program files), with an ad-hoc escape
264
+ * hatch for files created after the program was loaded.
265
+ */
266
+ class LsHost implements ts.LanguageServiceHost {
267
+ private readonly extraFiles = new Set<string>()
268
+
269
+ constructor(
270
+ private readonly program: ts.Program,
271
+ private readonly options: ts.CompilerOptions,
272
+ ) {}
273
+
274
+ includeExtraFile(fileName: string): void {
275
+ this.extraFiles.add(fileName)
276
+ }
277
+
278
+ getCompilationSettings(): ts.CompilerOptions {
279
+ return this.options
280
+ }
281
+
282
+ getScriptFileNames(): string[] {
283
+ const names = this.program.getSourceFiles().map((sf) => sf.fileName)
284
+ for (const extra of this.extraFiles) {
285
+ if (!names.includes(extra)) {
286
+ names.push(extra)
287
+ }
288
+ }
289
+ return names
290
+ }
291
+
292
+ getScriptVersion(fileName: string): string {
293
+ const sf = this.program.getSourceFile(fileName)
294
+ if (sf !== undefined) {
295
+ // Program files: the whole program + LS is rebuilt on any tracked
296
+ // change, so a static version is correct here.
297
+ return "1"
298
+ }
299
+ if (this.extraFiles.has(fileName)) {
300
+ // Version = mtime: a re-edit of an extra file invalidates the
301
+ // language service's cached parse of it.
302
+ try {
303
+ return String(fs.statSync(fileName).mtimeMs)
304
+ } catch {
305
+ return "1"
306
+ }
307
+ }
308
+ return ""
309
+ }
310
+
311
+ getScriptSnapshot(fileName: string): ts.IScriptSnapshot | undefined {
312
+ const sf = this.program.getSourceFile(fileName)
313
+ if (sf !== undefined) {
314
+ return ts.ScriptSnapshot.fromString(sf.text)
315
+ }
316
+ if (this.extraFiles.has(fileName)) {
317
+ const text = ts.sys.readFile(fileName)
318
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text)
319
+ }
320
+ return undefined
321
+ }
322
+
323
+ getCurrentDirectory(): string {
324
+ return this.program.getCurrentDirectory()
325
+ }
326
+
327
+ getDefaultLibFileName(options: ts.CompilerOptions): string {
328
+ return ts.getDefaultLibFilePath(options)
329
+ }
330
+
331
+ fileExists(fileName: string): boolean {
332
+ return ts.sys.fileExists(fileName)
333
+ }
334
+
335
+ readFile(fileName: string): string | undefined {
336
+ return ts.sys.readFile(fileName)
337
+ }
338
+
339
+ readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
340
+ return ts.sys.readDirectory(path, extensions, exclude, include, depth)
341
+ }
342
+
343
+ useCaseSensitiveFileNames(): boolean {
344
+ return ts.sys.useCaseSensitiveFileNames
345
+ }
346
+
347
+ getScriptKind(fileName: string): ts.ScriptKind {
348
+ return scriptKindFromFileName(fileName)
349
+ }
350
+ }
@@ -0,0 +1,213 @@
1
+ /**
2
+ * `rename_symbol` analysis: rename a symbol at (file, line[, character |
3
+ * symbol]) to a new name across EVERY real usage site in the workspace, as
4
+ * ONE atomic operation.
5
+ *
6
+ * Site enumeration reuses `findReferences` — the language service's real
7
+ * `getReferencesAtPosition` resolution, never grep — so the same "which
8
+ * identifiers are actually this symbol" correctness that `find_references`
9
+ * already has applies to the edit side too. Nothing here re-parses or
10
+ * guesses: the reference spans ARE the edit targets.
11
+ *
12
+ * Atomicity contract: either every site updates or none do.
13
+ * - All files are read and all new contents are COMPUTED before the first
14
+ * write, so a mismatch (file changed since the program snapshot) aborts
15
+ * with nothing written.
16
+ * - The writes then go file-by-file; if any write fails, every file
17
+ * already written is restored from its captured original before the
18
+ * error propagates. A partial rename leaving the codebase inconsistent
19
+ * is worse than the N-round-trip status quo this replaces.
20
+ *
21
+ * Scope guard (matches find_references/go_to_definition's boundary — see
22
+ * src/codeintel/program.ts): TypeScript/JavaScript source files only, and
23
+ * every site must resolve inside the workspace root. A reference outside
24
+ * the workspace (e.g. into node_modules) REFUSES the whole rename rather
25
+ * than silently skipping a site or editing outside the sandbox.
26
+ */
27
+
28
+ import * as path from "node:path"
29
+ import * as ts from "typescript"
30
+
31
+ import { scriptKindFromFileName, type CodeIntel } from "./program.js"
32
+ import { identifierAtPosition } from "./position.js"
33
+ import { findReferences } from "./find-references.js"
34
+ import { readFile, writeFile } from "node:fs/promises"
35
+
36
+ /** A rename edit target, captured BEFORE any file is written. */
37
+ export interface RenameSite {
38
+ fileName: string
39
+ line: number
40
+ /** Write access (declaration / assignment) vs read access. */
41
+ isWriteAccess: boolean
42
+ /** The line's text as it looked BEFORE the rename (for the result). */
43
+ snippet: string
44
+ }
45
+
46
+ export interface RenameResult {
47
+ oldName: string
48
+ newName: string
49
+ /** Every edited site, pre-rename text (sorted by file, then line). */
50
+ sites: RenameSite[]
51
+ }
52
+
53
+ /** A valid TS identifier (what `new_name` must be). */
54
+ const IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/
55
+
56
+ export function isValidIdentifier(name: string): boolean {
57
+ return IDENTIFIER_RE.test(name)
58
+ }
59
+
60
+ /** Parse a file standalone (for files created after the program was loaded). */
61
+ function readStandalone(file: string): ts.SourceFile | undefined {
62
+ const text = ts.sys.readFile(file)
63
+ if (text === undefined) {
64
+ return undefined
65
+ }
66
+ return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKindFromFileName(file))
67
+ }
68
+
69
+ /** True when `p` resolves inside `root` (path-safety mirror of safeTarget). */
70
+ function isInside(root: string, p: string): boolean {
71
+ const rel = path.relative(root, p)
72
+ return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel))
73
+ }
74
+
75
+ /**
76
+ * Rename the symbol at `line` (1-based) in `file` to `newName` everywhere it
77
+ * is referenced across the workspace. Throws with a clear message on any
78
+ * pre-write validation failure (bad new name, no references, a reference
79
+ * outside the workspace, a span that no longer matches the old name) and on
80
+ * a mid-write failure (after rolling back the files already written).
81
+ */
82
+ export async function renameSymbol(
83
+ intel: CodeIntel,
84
+ workspaceRoot: string,
85
+ file: string,
86
+ line: number,
87
+ character: number | undefined,
88
+ symbol: string | undefined,
89
+ newName: string,
90
+ ): Promise<RenameResult> {
91
+ const sourceFile = intel.program.getSourceFile(file) ?? readStandalone(file)
92
+ if (sourceFile === undefined) {
93
+ throw new Error(`file is not part of the loaded TypeScript program: ${file}`)
94
+ }
95
+ const identifier = identifierAtPosition(sourceFile, line, character, symbol)
96
+ if (identifier === undefined) {
97
+ throw new Error(`no identifier found at ${rel(workspaceRoot, file)}:${line} — nothing to rename`)
98
+ }
99
+ const oldName = identifier.text
100
+ if (oldName === newName) {
101
+ throw new Error(`new name must differ from the current name '${oldName}'`)
102
+ }
103
+ if (!isValidIdentifier(newName)) {
104
+ throw new Error(`'${newName}' is not a valid TypeScript identifier — use a plain name like 'renamedFoo'`)
105
+ }
106
+
107
+ intel.includeExtraFile(file)
108
+ const sites = findReferences(intel, file, line, character, symbol)
109
+ if (sites.length === 0) {
110
+ throw new Error(`no references found for '${oldName}' — nothing to rename`)
111
+ }
112
+
113
+ // Group the absolute edit offsets by file, refusing any site outside the
114
+ // workspace BEFORE anything is read or written (all-or-nothing).
115
+ const offsetsByFile = new Map<string, number[]>()
116
+ for (const site of sites) {
117
+ if (!isInside(workspaceRoot, site.fileName)) {
118
+ throw new Error(
119
+ `rename refused: reference to '${oldName}' at ${rel(workspaceRoot, site.fileName)}:${site.line} is outside the workspace root — renaming would edit outside the sandbox`,
120
+ )
121
+ }
122
+ const list = offsetsByFile.get(site.fileName) ?? []
123
+ list.push(site.start)
124
+ offsetsByFile.set(site.fileName, list)
125
+ }
126
+
127
+ // Phase 1: read every target file. A read failure aborts with nothing
128
+ // written — the file may have been deleted/renamed since the snapshot.
129
+ const originals = new Map<string, string>()
130
+ for (const fileName of offsetsByFile.keys()) {
131
+ try {
132
+ originals.set(fileName, await readFile(fileName, "utf-8"))
133
+ } catch (err) {
134
+ throw new Error(
135
+ `rename aborted: cannot read ${rel(workspaceRoot, fileName)} for the rename: ${err instanceof Error ? err.message : String(err)}`,
136
+ )
137
+ }
138
+ }
139
+
140
+ // Phase 2: compute every updated content in memory. Each span must still
141
+ // hold the old name — a mismatch means the file changed since the
142
+ // reference snapshot (the program would normally have rebuilt, but be
143
+ // defensive), and aborting here still writes nothing.
144
+ const updated = new Map<string, string>()
145
+ for (const [fileName, offsets] of offsetsByFile) {
146
+ const text = originals.get(fileName)!
147
+ // Descending order: later edits never shift earlier offsets.
148
+ const sorted = [...offsets].sort((a, b) => b - a)
149
+ let result = text
150
+ for (const start of sorted) {
151
+ const spanText = result.slice(start, start + oldName.length)
152
+ if (spanText !== oldName) {
153
+ throw new Error(
154
+ `rename aborted: expected '${oldName}' at ${rel(workspaceRoot, fileName)} (offset ${start}) but found '${spanText}' — the file changed since the reference snapshot; re-read it and re-run the rename`,
155
+ )
156
+ }
157
+ result = result.slice(0, start) + newName + result.slice(start + oldName.length)
158
+ }
159
+ updated.set(fileName, result)
160
+ }
161
+
162
+ // Phase 3: write every file; roll back the already-written ones on any
163
+ // failure so no partial rename survives.
164
+ const written: string[] = []
165
+ try {
166
+ for (const [fileName, content] of updated) {
167
+ await writeFile(fileName, content, "utf-8")
168
+ written.push(fileName)
169
+ }
170
+ } catch (err) {
171
+ for (const fileName of written) {
172
+ try {
173
+ await writeFile(fileName, originals.get(fileName)!, "utf-8")
174
+ } catch {
175
+ // Best-effort rollback; the primary error below is the signal.
176
+ }
177
+ }
178
+ throw new Error(
179
+ `rename failed while writing ${rel(workspaceRoot, written[written.length - 1] ?? "(unknown)")}: ${
180
+ err instanceof Error ? err.message : String(err)
181
+ } — all files already written were rolled back to their original content`,
182
+ )
183
+ }
184
+
185
+ const byFile = new Map<string, RenameSite[]>()
186
+ for (const site of sites) {
187
+ const list = byFile.get(site.fileName) ?? []
188
+ list.push({
189
+ fileName: site.fileName,
190
+ line: site.line,
191
+ isWriteAccess: site.isWriteAccess,
192
+ snippet: site.snippet,
193
+ })
194
+ byFile.set(site.fileName, list)
195
+ }
196
+ const flat = [...byFile.values()]
197
+ .flat()
198
+ .sort((a, b) => a.fileName.localeCompare(b.fileName) || a.line - b.line)
199
+
200
+ return { oldName, newName, sites: flat }
201
+ }
202
+
203
+ /** Workspace-relative (posix) display path; absolute if somehow outside. */
204
+ function rel(workspaceRoot: string, p: string): string {
205
+ const r = path.relative(workspaceRoot, p)
206
+ return r === "" || r.startsWith("..") ? p : r.split(path.sep).join("/")
207
+ }
208
+
209
+ /** Render a rename result as compact `file:line — snippet` lines. */
210
+ export function formatRenameResult(workspaceRoot: string, result: RenameResult): string {
211
+ const lines = result.sites.map((s) => `${rel(workspaceRoot, s.fileName)}:${s.line}${s.isWriteAccess ? " (write)" : ""} — ${s.snippet}`)
212
+ return `Renamed '${result.oldName}' → '${result.newName}' across ${result.sites.length} site(s):\n${lines.join("\n")}`
213
+ }