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.
- package/ATTRIBUTION.md +53 -0
- package/CODE_OF_CONDUCT.md +130 -0
- package/CONTRIBUTING.md +107 -0
- package/LICENSE +202 -0
- package/README.md +486 -0
- package/SECURITY.md +211 -0
- package/bin/headlesscode.mjs +83 -0
- package/package.json +63 -0
- package/shared/prompts/review-mode-prompt-short.md +93 -0
- package/shared/prompts/review-mode-prompt.md +281 -0
- package/shared/rules-code/rules.md +22 -0
- package/shared/stacks/cpp/rules.md +30 -0
- package/shared/stacks/fastapi/rules.md +30 -0
- package/shared/stacks/javascript/rules.md +37 -0
- package/shared/stacks/postgresql/rules.md +31 -0
- package/shared/stacks/python/rules.md +35 -0
- package/shared/stacks/react/rules.md +11 -0
- package/shared/stacks/typescript/rules.md +10 -0
- package/src/budget/budget.ts +221 -0
- package/src/budget/concurrency.ts +126 -0
- package/src/budget/cost.ts +309 -0
- package/src/budget/index.ts +8 -0
- package/src/checkpoints/cli.ts +256 -0
- package/src/checkpoints/service.ts +227 -0
- package/src/cli.ts +1535 -0
- package/src/cloud/docker-provider.ts +334 -0
- package/src/cloud/provider.ts +300 -0
- package/src/codeintel/call-graph.ts +78 -0
- package/src/codeintel/find-references.ts +123 -0
- package/src/codeintel/go-to-definition.ts +193 -0
- package/src/codeintel/handlers.ts +190 -0
- package/src/codeintel/import-graph.ts +173 -0
- package/src/codeintel/outline.ts +180 -0
- package/src/codeintel/position.ts +77 -0
- package/src/codeintel/program.ts +350 -0
- package/src/codeintel/rename-symbol.ts +213 -0
- package/src/codeintel/tools.ts +280 -0
- package/src/codemap/build.ts +135 -0
- package/src/codemap/cli.ts +190 -0
- package/src/codemap/extract.ts +339 -0
- package/src/codemap/files.ts +236 -0
- package/src/codemap/fingerprint.ts +65 -0
- package/src/codemap/flows.ts +62 -0
- package/src/codemap/html.ts +451 -0
- package/src/codemap/lock.ts +80 -0
- package/src/codemap/types.ts +101 -0
- package/src/codesearch/airunner-embedder.ts +185 -0
- package/src/codesearch/chunk.ts +339 -0
- package/src/codesearch/cli.ts +223 -0
- package/src/codesearch/embedder.ts +332 -0
- package/src/codesearch/files.ts +280 -0
- package/src/codesearch/index.ts +469 -0
- package/src/codesearch/ollama-embedder.ts +205 -0
- package/src/codesearch/search.ts +141 -0
- package/src/codesearch/types.ts +100 -0
- package/src/config/mode-models.ts +218 -0
- package/src/dashboard/aggregate.ts +364 -0
- package/src/dashboard/chat-thread.ts +141 -0
- package/src/dashboard/checkpoints.ts +124 -0
- package/src/dashboard/cli.ts +193 -0
- package/src/dashboard/codemap.ts +44 -0
- package/src/dashboard/files.ts +121 -0
- package/src/dashboard/page.ts +2803 -0
- package/src/dashboard/self-improvement-metrics.ts +282 -0
- package/src/dashboard/server.ts +1103 -0
- package/src/dashboard/session-launch.ts +310 -0
- package/src/dashboard/timeline.ts +273 -0
- package/src/dashboard/tool-exec.ts +107 -0
- package/src/dashboard/trend-cli.ts +141 -0
- package/src/dashboard/trend.ts +413 -0
- package/src/decision-proxy/cli.ts +261 -0
- package/src/decision-proxy/proxy.ts +569 -0
- package/src/deploy/gate-cli.ts +147 -0
- package/src/deploy/gate.ts +254 -0
- package/src/engine/condense.ts +512 -0
- package/src/engine/events.ts +428 -0
- package/src/engine/handoff.ts +71 -0
- package/src/engine/lazy-tools.ts +160 -0
- package/src/engine/local-explore.ts +653 -0
- package/src/engine/logger.ts +96 -0
- package/src/engine/loop.ts +5517 -0
- package/src/engine/parser.ts +347 -0
- package/src/engine/prompt.ts +860 -0
- package/src/engine/reports.ts +47 -0
- package/src/engine/stacks.ts +448 -0
- package/src/engine/types.ts +291 -0
- package/src/engine/usage.ts +186 -0
- package/src/github/app-auth.ts +161 -0
- package/src/github/cli.ts +448 -0
- package/src/github/installations.ts +133 -0
- package/src/github/pr.ts +321 -0
- package/src/github/provision.ts +118 -0
- package/src/github/push.ts +122 -0
- package/src/index-util.ts +50 -0
- package/src/index.ts +81 -0
- package/src/init/cli.ts +248 -0
- package/src/init/gitignore.ts +74 -0
- package/src/llm/ollama.ts +308 -0
- package/src/llm/openrouter.ts +868 -0
- package/src/llm/preflight.ts +367 -0
- package/src/llm/transcript-capture.ts +84 -0
- package/src/memory/embed.ts +110 -0
- package/src/memory/index.ts +22 -0
- package/src/memory/local.ts +259 -0
- package/src/memory/summarizer.ts +283 -0
- package/src/memory/types.ts +153 -0
- package/src/memory/uwuchat.ts +157 -0
- package/src/migrate/cli.ts +115 -0
- package/src/orchestrator/analyze-cli.ts +104 -0
- package/src/orchestrator/auto-split.ts +206 -0
- package/src/orchestrator/cleanup.ts +1003 -0
- package/src/orchestrator/cli.ts +3571 -0
- package/src/orchestrator/cost-estimate.ts +564 -0
- package/src/orchestrator/cost-history-cli.ts +242 -0
- package/src/orchestrator/cost-history.ts +397 -0
- package/src/orchestrator/git-sync.ts +250 -0
- package/src/orchestrator/index.ts +153 -0
- package/src/orchestrator/log-analysis.ts +0 -0
- package/src/orchestrator/merge-check.ts +108 -0
- package/src/orchestrator/pipeline.ts +411 -0
- package/src/orchestrator/resume.ts +1940 -0
- package/src/orchestrator/reviewer.ts +503 -0
- package/src/orchestrator/split.ts +296 -0
- package/src/orchestrator/state.ts +542 -0
- package/src/orchestrator/status.ts +697 -0
- package/src/orchestrator/verification-gate.ts +134 -0
- package/src/orchestrator/watch.ts +898 -0
- package/src/permissions/commands.ts +1083 -0
- package/src/permissions/config.ts +241 -0
- package/src/permissions/index.ts +12 -0
- package/src/permissions/protected-files.ts +96 -0
- package/src/permissions/store-protection.ts +272 -0
- package/src/project-store.ts +648 -0
- package/src/projects/cli.ts +382 -0
- package/src/qa/qa.ts +487 -0
- package/src/tools/browser/handler.ts +346 -0
- package/src/tools/browser/service.ts +406 -0
- package/src/tools/browser/smoke.ts +78 -0
- package/src/tools/browser/tool.ts +99 -0
- package/src/tools/executor.ts +2575 -0
- package/src/tools/language-detect.ts +183 -0
- package/src/tools/output-summarizer.ts +369 -0
- package/src/tools/run-tests.ts +302 -0
- package/src/tools/set-indentation-tool.ts +49 -0
- package/src/tools/test-selection.ts +160 -0
- package/src/vendor/tests/smoke.ts +103 -0
- package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
- package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
- package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
- package/src/vendor/zoo-code/shim/os-name.ts +18 -0
- package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
- package/src/vendor/zoo-code/shim/vscode.ts +76 -0
- package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
- package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
- package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
- package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
- package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
- package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
- package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
- package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
- package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
- package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
- package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
- package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
- package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
- package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
- package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
- package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
- package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
- package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
- package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
- package/src/vendor/zoo-code/src/shared/language.ts +43 -0
- package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
- package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
- package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
- package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
- package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
- package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
- package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
- package/src/vendor/zoo-code/src/utils/object.ts +18 -0
- package/src/vendor/zoo-code/src/utils/path.ts +94 -0
- package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
- package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
- package/src/vendor/zoo-code/types/global-settings.ts +19 -0
- package/src/vendor/zoo-code/types/index.ts +22 -0
- package/src/vendor/zoo-code/types/message.ts +375 -0
- package/src/vendor/zoo-code/types/mode.ts +241 -0
- package/src/vendor/zoo-code/types/todo.ts +19 -0
- package/src/vendor/zoo-code/types/tool-params.ts +116 -0
- package/src/vendor/zoo-code/types/tool.ts +67 -0
- package/src/vendor/zoo-code/types/vscode.ts +84 -0
- package/src/vision/describe.ts +242 -0
- package/src/vision/tool.ts +91 -0
- package/src/watcher/cli.ts +369 -0
- package/src/watcher/github.ts +304 -0
- package/src/watcher/index.ts +59 -0
- package/src/watcher/state.ts +254 -0
- package/src/watcher/watch.ts +562 -0
- 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
|
+
}
|