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,15 @@
|
|
|
1
|
+
import * as path from "path"
|
|
2
|
+
|
|
3
|
+
import { CheckpointServiceOptions } from "./types"
|
|
4
|
+
import { ShadowCheckpointService } from "./ShadowCheckpointService"
|
|
5
|
+
|
|
6
|
+
export class RepoPerTaskCheckpointService extends ShadowCheckpointService {
|
|
7
|
+
public static create({ taskId, workspaceDir, shadowDir, log = console.log }: CheckpointServiceOptions) {
|
|
8
|
+
return new RepoPerTaskCheckpointService(
|
|
9
|
+
taskId,
|
|
10
|
+
path.join(shadowDir, "tasks", taskId, "checkpoints"),
|
|
11
|
+
workspaceDir,
|
|
12
|
+
log,
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import fs from "fs/promises"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import * as path from "path"
|
|
4
|
+
import crypto from "crypto"
|
|
5
|
+
import EventEmitter from "events"
|
|
6
|
+
|
|
7
|
+
import simpleGit, { SimpleGit, SimpleGitOptions } from "simple-git"
|
|
8
|
+
import pWaitFor from "p-wait-for"
|
|
9
|
+
import * as vscode from "../../../shim/vscode"
|
|
10
|
+
|
|
11
|
+
import { fileExistsAtPath } from "../../utils/fs"
|
|
12
|
+
import { arePathsEqual } from "../../utils/path"
|
|
13
|
+
import { executeRipgrep } from "../../services/search/file-search"
|
|
14
|
+
import { t } from "../../i18n"
|
|
15
|
+
|
|
16
|
+
import { CheckpointDiff, CheckpointResult, CheckpointEventMap } from "./types"
|
|
17
|
+
import { getExcludePatterns } from "./excludes"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Environment variables stripped before passing the env to simple-git.
|
|
21
|
+
*
|
|
22
|
+
* Two categories:
|
|
23
|
+
* - Location-override vars (GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_OBJECT_DIRECTORY,
|
|
24
|
+
* GIT_ALTERNATE_OBJECT_DIRECTORIES, GIT_CEILING_DIRECTORIES): redirect git operations to
|
|
25
|
+
* unintended repositories or limit where git searches.
|
|
26
|
+
* - Code-execution vectors blocked by simple-git ≥3.36's blockUnsafeOperationsPlugin when
|
|
27
|
+
* passed via .env(): GIT_EDITOR, GIT_SSH_COMMAND, GIT_PAGER, PREFIX, etc.
|
|
28
|
+
*
|
|
29
|
+
* Stripping GIT_CONFIG_COUNT also neutralises the entire GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n
|
|
30
|
+
* family — git ignores those per-key entries when the count key is absent.
|
|
31
|
+
*/
|
|
32
|
+
export const BLOCKED_ENV_KEYS = new Set([
|
|
33
|
+
"GIT_DIR",
|
|
34
|
+
"GIT_WORK_TREE",
|
|
35
|
+
"GIT_INDEX_FILE",
|
|
36
|
+
"GIT_OBJECT_DIRECTORY",
|
|
37
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
38
|
+
"GIT_CEILING_DIRECTORIES",
|
|
39
|
+
"GIT_TEMPLATE_DIR",
|
|
40
|
+
"GIT_EDITOR",
|
|
41
|
+
"GIT_SEQUENCE_EDITOR",
|
|
42
|
+
"GIT_ASKPASS",
|
|
43
|
+
"GIT_SSH",
|
|
44
|
+
"GIT_SSH_COMMAND",
|
|
45
|
+
"GIT_PAGER",
|
|
46
|
+
"GIT_PROXY_COMMAND",
|
|
47
|
+
"GIT_EXEC_PATH",
|
|
48
|
+
"GIT_EXTERNAL_DIFF",
|
|
49
|
+
"GIT_CONFIG",
|
|
50
|
+
"GIT_CONFIG_GLOBAL",
|
|
51
|
+
"GIT_CONFIG_SYSTEM",
|
|
52
|
+
"GIT_CONFIG_COUNT",
|
|
53
|
+
"PREFIX",
|
|
54
|
+
"EDITOR",
|
|
55
|
+
"PAGER",
|
|
56
|
+
"SSH_ASKPASS",
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
// Lowercase set for case-insensitive lookup — the plugin uses toLowerCase() internally,
|
|
60
|
+
// so a var like Git_Editor would bypass an exact-match check on Linux.
|
|
61
|
+
const BLOCKED_ENV_KEYS_LOWER = new Set([...BLOCKED_ENV_KEYS].map((k) => k.toLowerCase()))
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a SimpleGit instance with sanitized environment variables to prevent
|
|
65
|
+
* interference from inherited git environment variables like GIT_DIR and GIT_WORK_TREE.
|
|
66
|
+
* This ensures checkpoint operations always target the intended shadow repository.
|
|
67
|
+
*
|
|
68
|
+
* @param baseDir - The directory where git operations should be executed
|
|
69
|
+
* @returns A SimpleGit instance with sanitized environment
|
|
70
|
+
*/
|
|
71
|
+
function createSanitizedGit(baseDir: string): SimpleGit {
|
|
72
|
+
const sanitizedEnv: Record<string, string> = {}
|
|
73
|
+
const removedKeys: string[] = []
|
|
74
|
+
|
|
75
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
76
|
+
if (BLOCKED_ENV_KEYS_LOWER.has(key.toLowerCase())) {
|
|
77
|
+
removedKeys.push(key)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (value !== undefined) {
|
|
82
|
+
sanitizedEnv[key] = value
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Log which git env vars were removed (helps with debugging Dev Container issues)
|
|
87
|
+
if (removedKeys.length > 0) {
|
|
88
|
+
console.log(
|
|
89
|
+
`[createSanitizedGit] Removed git environment variables for checkpoint isolation: ${removedKeys.join(", ")}`,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const options: Partial<SimpleGitOptions> = {
|
|
94
|
+
baseDir,
|
|
95
|
+
config: [],
|
|
96
|
+
// --template="" stops git copying hooks/templates into the shadow repo (axis 1).
|
|
97
|
+
// GIT_TEMPLATE_DIR is stripped from the env above to block the env-var path (axis 2).
|
|
98
|
+
// allowUnsafeTemplateDir opts out of simple-git ≥3.36's blockUnsafeOperationsPlugin
|
|
99
|
+
// so the --template arg is not rejected before reaching git.
|
|
100
|
+
unsafe: { allowUnsafeTemplateDir: true },
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Create git instance and set the sanitized environment
|
|
104
|
+
const git = simpleGit(options)
|
|
105
|
+
|
|
106
|
+
// Use the .env() method to set the complete sanitized environment
|
|
107
|
+
// This replaces the inherited environment with our sanitized version
|
|
108
|
+
git.env(sanitizedEnv)
|
|
109
|
+
|
|
110
|
+
console.log(`[createSanitizedGit] Created git instance for baseDir: ${baseDir}`)
|
|
111
|
+
|
|
112
|
+
return git
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export abstract class ShadowCheckpointService extends EventEmitter {
|
|
116
|
+
public readonly taskId: string
|
|
117
|
+
public readonly checkpointsDir: string
|
|
118
|
+
public readonly workspaceDir: string
|
|
119
|
+
|
|
120
|
+
protected _checkpoints: string[] = []
|
|
121
|
+
protected _baseHash?: string
|
|
122
|
+
|
|
123
|
+
protected readonly dotGitDir: string
|
|
124
|
+
protected git?: SimpleGit
|
|
125
|
+
protected readonly log: (message: string) => void
|
|
126
|
+
protected shadowGitConfigWorktree?: string
|
|
127
|
+
|
|
128
|
+
public get baseHash() {
|
|
129
|
+
return this._baseHash
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected set baseHash(value: string | undefined) {
|
|
133
|
+
this._baseHash = value
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public get isInitialized() {
|
|
137
|
+
return !!this.git
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
public getCheckpoints(): string[] {
|
|
141
|
+
return this._checkpoints.slice()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) {
|
|
145
|
+
super()
|
|
146
|
+
|
|
147
|
+
const homedir = os.homedir()
|
|
148
|
+
const desktopPath = path.join(homedir, "Desktop")
|
|
149
|
+
const documentsPath = path.join(homedir, "Documents")
|
|
150
|
+
const downloadsPath = path.join(homedir, "Downloads")
|
|
151
|
+
const protectedPaths = [homedir, desktopPath, documentsPath, downloadsPath]
|
|
152
|
+
|
|
153
|
+
if (protectedPaths.includes(workspaceDir)) {
|
|
154
|
+
throw new Error(`Cannot use checkpoints in ${workspaceDir}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
this.taskId = taskId
|
|
158
|
+
this.checkpointsDir = checkpointsDir
|
|
159
|
+
this.workspaceDir = workspaceDir
|
|
160
|
+
|
|
161
|
+
this.dotGitDir = path.join(this.checkpointsDir, ".git")
|
|
162
|
+
this.log = log
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public async initShadowGit(onInit?: () => Promise<void>) {
|
|
166
|
+
if (this.git) {
|
|
167
|
+
throw new Error("Shadow git repo already initialized")
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const nestedGitPath = await this.getNestedGitRepository()
|
|
171
|
+
|
|
172
|
+
if (nestedGitPath) {
|
|
173
|
+
// Show persistent error message with the offending path
|
|
174
|
+
const relativePath = path.relative(this.workspaceDir, nestedGitPath)
|
|
175
|
+
const message = t("common:errors.nested_git_repos_warning", { path: relativePath })
|
|
176
|
+
vscode.window.showErrorMessage(message)
|
|
177
|
+
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Checkpoints are disabled because a nested git repository was detected at: ${relativePath}. ` +
|
|
180
|
+
"Please remove or relocate nested git repositories to use the checkpoints feature.",
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
await fs.mkdir(this.checkpointsDir, { recursive: true })
|
|
185
|
+
const git = createSanitizedGit(this.checkpointsDir)
|
|
186
|
+
const gitVersion = await git.version()
|
|
187
|
+
this.log(`[${this.constructor.name}#create] git = ${gitVersion}`)
|
|
188
|
+
|
|
189
|
+
let created = false
|
|
190
|
+
const startTime = Date.now()
|
|
191
|
+
|
|
192
|
+
if (await fileExistsAtPath(this.dotGitDir)) {
|
|
193
|
+
this.log(`[${this.constructor.name}#initShadowGit] shadow git repo already exists at ${this.dotGitDir}`)
|
|
194
|
+
const worktree = await this.getShadowGitConfigWorktree(git)
|
|
195
|
+
|
|
196
|
+
if (!worktree) {
|
|
197
|
+
throw new Error("Checkpoints require core.worktree to be set in the shadow git config")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const worktreeTrimmed = worktree.trim()
|
|
201
|
+
|
|
202
|
+
if (!arePathsEqual(worktreeTrimmed, this.workspaceDir)) {
|
|
203
|
+
throw new Error(
|
|
204
|
+
`Checkpoints can only be used in the original workspace: ${worktreeTrimmed} !== ${this.workspaceDir}`,
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await this.writeExcludeFile()
|
|
209
|
+
this.baseHash = await git.revparse(["HEAD"])
|
|
210
|
+
} else {
|
|
211
|
+
this.log(`[${this.constructor.name}#initShadowGit] creating shadow git repo at ${this.checkpointsDir}`)
|
|
212
|
+
await git.init({ "--template": "" })
|
|
213
|
+
await git.addConfig("core.worktree", this.workspaceDir) // Sets the working tree to the current workspace.
|
|
214
|
+
await git.addConfig("commit.gpgSign", "false") // Disable commit signing for shadow repo.
|
|
215
|
+
await git.addConfig("user.name", "Roo Code")
|
|
216
|
+
await git.addConfig("user.email", "noreply@example.com")
|
|
217
|
+
await this.writeExcludeFile()
|
|
218
|
+
await this.stageAll(git)
|
|
219
|
+
const { commit } = await git.commit("initial commit", { "--allow-empty": null })
|
|
220
|
+
this.baseHash = commit
|
|
221
|
+
created = true
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const duration = Date.now() - startTime
|
|
225
|
+
|
|
226
|
+
this.log(
|
|
227
|
+
`[${this.constructor.name}#initShadowGit] initialized shadow repo with base commit ${this.baseHash} in ${duration}ms`,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
this.git = git
|
|
231
|
+
|
|
232
|
+
await onInit?.()
|
|
233
|
+
|
|
234
|
+
this.emit("initialize", {
|
|
235
|
+
type: "initialize",
|
|
236
|
+
workspaceDir: this.workspaceDir,
|
|
237
|
+
baseHash: this.baseHash,
|
|
238
|
+
created,
|
|
239
|
+
duration,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
return { created, duration }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Add basic excludes directly in git config, while respecting any
|
|
246
|
+
// .gitignore in the workspace.
|
|
247
|
+
// .git/info/exclude is local to the shadow git repo, so it's not
|
|
248
|
+
// shared with the main repo - and won't conflict with user's
|
|
249
|
+
// .gitignore.
|
|
250
|
+
protected async writeExcludeFile() {
|
|
251
|
+
await fs.mkdir(path.join(this.dotGitDir, "info"), { recursive: true })
|
|
252
|
+
const patterns = await getExcludePatterns(this.workspaceDir)
|
|
253
|
+
await fs.writeFile(path.join(this.dotGitDir, "info", "exclude"), patterns.join("\n"))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async stageAll(git: SimpleGit) {
|
|
257
|
+
try {
|
|
258
|
+
await git.add([".", "--ignore-errors"])
|
|
259
|
+
} catch (error) {
|
|
260
|
+
this.log(
|
|
261
|
+
`[${this.constructor.name}#stageAll] failed to add files to git: ${error instanceof Error ? error.message : String(error)}`,
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async getNestedGitRepository(): Promise<string | null> {
|
|
267
|
+
try {
|
|
268
|
+
// Find all .git/HEAD files that are not at the root level.
|
|
269
|
+
const args = ["--files", "--hidden", "--follow", "-g", "**/.git/HEAD", this.workspaceDir]
|
|
270
|
+
|
|
271
|
+
const gitPaths = await executeRipgrep({ args, workspacePath: this.workspaceDir })
|
|
272
|
+
|
|
273
|
+
// Filter to only include nested git directories (not the root .git).
|
|
274
|
+
// Since we're searching for HEAD files, we expect type to be "file"
|
|
275
|
+
const nestedGitPaths = gitPaths.filter(({ type, path: filePath }) => {
|
|
276
|
+
// Check if it's a file and is a nested .git/HEAD (not at root)
|
|
277
|
+
if (type !== "file") return false
|
|
278
|
+
|
|
279
|
+
// Ensure it's a .git/HEAD file and not the root one
|
|
280
|
+
const normalizedPath = filePath.replace(/\\/g, "/")
|
|
281
|
+
return (
|
|
282
|
+
normalizedPath.includes(".git/HEAD") &&
|
|
283
|
+
!normalizedPath.startsWith(".git/") &&
|
|
284
|
+
normalizedPath !== ".git/HEAD"
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
if (nestedGitPaths.length > 0) {
|
|
289
|
+
// Get the first nested git repository path
|
|
290
|
+
// Remove .git/HEAD from the path to get the repository directory
|
|
291
|
+
const headPath = nestedGitPaths[0].path
|
|
292
|
+
|
|
293
|
+
// Use path module to properly extract the repository directory
|
|
294
|
+
// The HEAD file is at .git/HEAD, so we need to go up two directories
|
|
295
|
+
const gitDir = path.dirname(headPath) // removes HEAD, gives us .git
|
|
296
|
+
const repoDir = path.dirname(gitDir) // removes .git, gives us the repo directory
|
|
297
|
+
|
|
298
|
+
const absolutePath = path.join(this.workspaceDir, repoDir)
|
|
299
|
+
|
|
300
|
+
this.log(
|
|
301
|
+
`[${this.constructor.name}#getNestedGitRepository] found ${nestedGitPaths.length} nested git repositories, first at: ${repoDir}`,
|
|
302
|
+
)
|
|
303
|
+
return absolutePath
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return null
|
|
307
|
+
} catch (error) {
|
|
308
|
+
this.log(
|
|
309
|
+
`[${this.constructor.name}#getNestedGitRepository] failed to check for nested git repos: ${error instanceof Error ? error.message : String(error)}`,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
// If we can't check, assume there are no nested repos to avoid blocking the feature.
|
|
313
|
+
return null
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async getShadowGitConfigWorktree(git: SimpleGit) {
|
|
318
|
+
if (!this.shadowGitConfigWorktree) {
|
|
319
|
+
try {
|
|
320
|
+
this.shadowGitConfigWorktree = (await git.getConfig("core.worktree")).value || undefined
|
|
321
|
+
} catch (error) {
|
|
322
|
+
this.log(
|
|
323
|
+
`[${this.constructor.name}#getShadowGitConfigWorktree] failed to get core.worktree: ${error instanceof Error ? error.message : String(error)}`,
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return this.shadowGitConfigWorktree
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public async saveCheckpoint(
|
|
332
|
+
message: string,
|
|
333
|
+
options?: { allowEmpty?: boolean; suppressMessage?: boolean },
|
|
334
|
+
): Promise<CheckpointResult | undefined> {
|
|
335
|
+
try {
|
|
336
|
+
this.log(
|
|
337
|
+
`[${this.constructor.name}#saveCheckpoint] starting checkpoint save (allowEmpty: ${options?.allowEmpty ?? false})`,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if (!this.git) {
|
|
341
|
+
throw new Error("Shadow git repo not initialized")
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const startTime = Date.now()
|
|
345
|
+
await this.stageAll(this.git)
|
|
346
|
+
const commitArgs = options?.allowEmpty ? { "--allow-empty": null } : undefined
|
|
347
|
+
const result = await this.git.commit(message, commitArgs)
|
|
348
|
+
const fromHash = this._checkpoints[this._checkpoints.length - 1] ?? this.baseHash!
|
|
349
|
+
const toHash = result.commit || fromHash
|
|
350
|
+
this._checkpoints.push(toHash)
|
|
351
|
+
const duration = Date.now() - startTime
|
|
352
|
+
|
|
353
|
+
if (result.commit) {
|
|
354
|
+
this.emit("checkpoint", {
|
|
355
|
+
type: "checkpoint",
|
|
356
|
+
fromHash,
|
|
357
|
+
toHash,
|
|
358
|
+
duration,
|
|
359
|
+
suppressMessage: options?.suppressMessage ?? false,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (result.commit) {
|
|
364
|
+
this.log(
|
|
365
|
+
`[${this.constructor.name}#saveCheckpoint] checkpoint saved in ${duration}ms -> ${result.commit}`,
|
|
366
|
+
)
|
|
367
|
+
return result
|
|
368
|
+
} else {
|
|
369
|
+
this.log(`[${this.constructor.name}#saveCheckpoint] found no changes to commit in ${duration}ms`)
|
|
370
|
+
return undefined
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
const error = e instanceof Error ? e : new Error(String(e))
|
|
374
|
+
this.log(`[${this.constructor.name}#saveCheckpoint] failed to create checkpoint: ${error.message}`)
|
|
375
|
+
this.emit("error", { type: "error", error })
|
|
376
|
+
throw error
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
public async restoreCheckpoint(commitHash: string) {
|
|
381
|
+
try {
|
|
382
|
+
this.log(`[${this.constructor.name}#restoreCheckpoint] starting checkpoint restore`)
|
|
383
|
+
|
|
384
|
+
if (!this.git) {
|
|
385
|
+
throw new Error("Shadow git repo not initialized")
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const start = Date.now()
|
|
389
|
+
await this.git.clean("f", ["-d", "-f"])
|
|
390
|
+
await this.git.reset(["--hard", commitHash])
|
|
391
|
+
|
|
392
|
+
// Remove all checkpoints after the specified commitHash.
|
|
393
|
+
const checkpointIndex = this._checkpoints.indexOf(commitHash)
|
|
394
|
+
|
|
395
|
+
if (checkpointIndex !== -1) {
|
|
396
|
+
this._checkpoints = this._checkpoints.slice(0, checkpointIndex + 1)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const duration = Date.now() - start
|
|
400
|
+
this.emit("restore", { type: "restore", commitHash, duration })
|
|
401
|
+
this.log(`[${this.constructor.name}#restoreCheckpoint] restored checkpoint ${commitHash} in ${duration}ms`)
|
|
402
|
+
} catch (e) {
|
|
403
|
+
const error = e instanceof Error ? e : new Error(String(e))
|
|
404
|
+
this.log(`[${this.constructor.name}#restoreCheckpoint] failed to restore checkpoint: ${error.message}`)
|
|
405
|
+
this.emit("error", { type: "error", error })
|
|
406
|
+
throw error
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
public async getDiff({ from, to }: { from?: string; to?: string }): Promise<CheckpointDiff[]> {
|
|
411
|
+
if (!this.git) {
|
|
412
|
+
throw new Error("Shadow git repo not initialized")
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const result = []
|
|
416
|
+
|
|
417
|
+
if (!from) {
|
|
418
|
+
from = (await this.git.raw(["rev-list", "--max-parents=0", "HEAD"])).trim()
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Stage all changes so that untracked files appear in diff summary.
|
|
422
|
+
await this.stageAll(this.git)
|
|
423
|
+
|
|
424
|
+
this.log(`[${this.constructor.name}#getDiff] diffing ${to ? `${from}..${to}` : `${from}..HEAD`}`)
|
|
425
|
+
const { files } = to ? await this.git.diffSummary([`${from}..${to}`]) : await this.git.diffSummary([from])
|
|
426
|
+
|
|
427
|
+
const cwdPath = (await this.getShadowGitConfigWorktree(this.git)) || this.workspaceDir || ""
|
|
428
|
+
|
|
429
|
+
for (const file of files) {
|
|
430
|
+
const relPath = file.file
|
|
431
|
+
const absPath = path.join(cwdPath, relPath)
|
|
432
|
+
const before = await this.git.show([`${from}:${relPath}`]).catch(() => "")
|
|
433
|
+
|
|
434
|
+
const after = to
|
|
435
|
+
? await this.git.show([`${to}:${relPath}`]).catch(() => "")
|
|
436
|
+
: await fs.readFile(absPath, "utf8").catch(() => "")
|
|
437
|
+
|
|
438
|
+
result.push({ paths: { relative: relPath, absolute: absPath }, content: { before, after } })
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return result
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* EventEmitter
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
override emit<K extends keyof CheckpointEventMap>(event: K, data: CheckpointEventMap[K]) {
|
|
449
|
+
return super.emit(event, data)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
override on<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
|
|
453
|
+
return super.on(event, listener)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
override off<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
|
|
457
|
+
return super.off(event, listener)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
override once<K extends keyof CheckpointEventMap>(event: K, listener: (data: CheckpointEventMap[K]) => void) {
|
|
461
|
+
return super.once(event, listener)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Storage
|
|
466
|
+
*/
|
|
467
|
+
|
|
468
|
+
public static hashWorkspaceDir(workspaceDir: string) {
|
|
469
|
+
return crypto.createHash("sha256").update(workspaceDir).digest("hex").toString().slice(0, 8)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
protected static taskRepoDir({ taskId, globalStorageDir }: { taskId: string; globalStorageDir: string }) {
|
|
473
|
+
return path.join(globalStorageDir, "tasks", taskId, "checkpoints")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
protected static workspaceRepoDir({
|
|
477
|
+
globalStorageDir,
|
|
478
|
+
workspaceDir,
|
|
479
|
+
}: {
|
|
480
|
+
globalStorageDir: string
|
|
481
|
+
workspaceDir: string
|
|
482
|
+
}) {
|
|
483
|
+
return path.join(globalStorageDir, "checkpoints", this.hashWorkspaceDir(workspaceDir))
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
public static async deleteTask({
|
|
487
|
+
taskId,
|
|
488
|
+
globalStorageDir,
|
|
489
|
+
workspaceDir,
|
|
490
|
+
}: {
|
|
491
|
+
taskId: string
|
|
492
|
+
globalStorageDir: string
|
|
493
|
+
workspaceDir: string
|
|
494
|
+
}) {
|
|
495
|
+
const workspaceRepoDir = this.workspaceRepoDir({ globalStorageDir, workspaceDir })
|
|
496
|
+
const branchName = `roo-${taskId}`
|
|
497
|
+
const git = createSanitizedGit(workspaceRepoDir)
|
|
498
|
+
const success = await this.deleteBranch(git, branchName)
|
|
499
|
+
|
|
500
|
+
if (success) {
|
|
501
|
+
console.log(`[${this.name}#deleteTask.${taskId}] deleted branch ${branchName}`)
|
|
502
|
+
} else {
|
|
503
|
+
console.error(`[${this.name}#deleteTask.${taskId}] failed to delete branch ${branchName}`)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
public static async deleteBranch(git: SimpleGit, branchName: string) {
|
|
508
|
+
const branches = await git.branchLocal()
|
|
509
|
+
|
|
510
|
+
if (!branches.all.includes(branchName)) {
|
|
511
|
+
console.error(`[${this.constructor.name}#deleteBranch] branch ${branchName} does not exist`)
|
|
512
|
+
return false
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const currentBranch = await git.revparse(["--abbrev-ref", "HEAD"])
|
|
516
|
+
|
|
517
|
+
if (currentBranch === branchName) {
|
|
518
|
+
const worktree = await git.getConfig("core.worktree")
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
await git.raw(["config", "--unset", "core.worktree"])
|
|
522
|
+
await git.reset(["--hard"])
|
|
523
|
+
await git.clean("f", ["-d"])
|
|
524
|
+
const defaultBranch = branches.all.includes("main") ? "main" : "master"
|
|
525
|
+
await git.checkout([defaultBranch, "--force"])
|
|
526
|
+
|
|
527
|
+
await pWaitFor(
|
|
528
|
+
async () => {
|
|
529
|
+
const newBranch = await git.revparse(["--abbrev-ref", "HEAD"])
|
|
530
|
+
return newBranch === defaultBranch
|
|
531
|
+
},
|
|
532
|
+
{ interval: 500, timeout: 2_000 },
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
await git.branch(["-D", branchName])
|
|
536
|
+
return true
|
|
537
|
+
} catch (error) {
|
|
538
|
+
console.error(
|
|
539
|
+
`[${this.constructor.name}#deleteBranch] failed to delete branch ${branchName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
return false
|
|
543
|
+
} finally {
|
|
544
|
+
if (worktree.value) {
|
|
545
|
+
await git.addConfig("core.worktree", worktree.value)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
} else {
|
|
549
|
+
await git.branch(["-D", branchName])
|
|
550
|
+
return true
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|