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,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode checkpoints` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* npx tsx src/cli.ts checkpoints --workspace <path> list
|
|
5
|
+
* npx tsx src/cli.ts checkpoints --workspace <path> restore <hash>
|
|
6
|
+
* npx tsx src/cli.ts checkpoints --workspace <path> diff <hash> [<to-hash>]
|
|
7
|
+
*
|
|
8
|
+
* A human-facing wrapper around src/checkpoints/service.ts. Since each
|
|
9
|
+
* `HeadlessSession` gets its own random taskId (see src/engine/loop.ts), a
|
|
10
|
+
* workspace can have checkpoints from multiple past sessions. This CLI
|
|
11
|
+
* operates on the MOST RECENT session's checkpoints for the workspace (the
|
|
12
|
+
* one most likely relevant to "undo my last run") — found by scanning
|
|
13
|
+
* `<checkpointDir>/tasks/*\/checkpoints` for the shadow repo whose
|
|
14
|
+
* `core.worktree` matches `--workspace` and picking the one with the newest
|
|
15
|
+
* commit. Pass `--task-id <id>` to target a specific session explicitly.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { execFileSync } from "node:child_process"
|
|
19
|
+
import * as fs from "node:fs"
|
|
20
|
+
import * as path from "node:path"
|
|
21
|
+
|
|
22
|
+
import { createCheckpointService, defaultCheckpointDir, type CheckpointService } from "./service.js"
|
|
23
|
+
|
|
24
|
+
const CHECKPOINTS_USAGE = `headlesscode checkpoints — inspect/revert shadow-git checkpoints
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
headlesscode checkpoints --workspace <path> list
|
|
28
|
+
headlesscode checkpoints --workspace <path> restore <hash>
|
|
29
|
+
headlesscode checkpoints --workspace <path> diff [<from-hash>] [<to-hash>]
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--workspace <path> Workspace root whose checkpoints to inspect (required)
|
|
33
|
+
--checkpoint-dir <path> Shadow-git storage root (default: ${defaultCheckpointDir()})
|
|
34
|
+
--task-id <id> Operate on this session's checkpoints instead of
|
|
35
|
+
auto-detecting the most recent one for --workspace
|
|
36
|
+
--help Show this help and exit
|
|
37
|
+
|
|
38
|
+
Commands:
|
|
39
|
+
list List checkpoints for the workspace, oldest-first
|
|
40
|
+
(hash, date, message)
|
|
41
|
+
restore <hash> Check the shadow ref out onto the real workspace,
|
|
42
|
+
reverting files to that checkpoint. DESTRUCTIVE:
|
|
43
|
+
uncommitted changes since that checkpoint are lost.
|
|
44
|
+
diff [<from>] [<to>] Show changed files between two checkpoints (default
|
|
45
|
+
from: the first checkpoint; default to: the current
|
|
46
|
+
working tree)
|
|
47
|
+
`
|
|
48
|
+
|
|
49
|
+
interface CheckpointsCliOptions {
|
|
50
|
+
workspace?: string
|
|
51
|
+
checkpointDir?: string
|
|
52
|
+
taskId?: string
|
|
53
|
+
help: boolean
|
|
54
|
+
command?: "list" | "restore" | "diff"
|
|
55
|
+
commandArgs: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseCheckpointsArgs(argv: string[]): { options: CheckpointsCliOptions; error?: string } {
|
|
59
|
+
const options: CheckpointsCliOptions = { help: false, commandArgs: [] }
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < argv.length; i++) {
|
|
62
|
+
const arg = argv[i]
|
|
63
|
+
const eq = arg.indexOf("=")
|
|
64
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
65
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
66
|
+
const next = (): string | undefined => {
|
|
67
|
+
if (inlineValue !== undefined) {
|
|
68
|
+
return inlineValue
|
|
69
|
+
}
|
|
70
|
+
const v = argv[i + 1]
|
|
71
|
+
if (v === undefined || v.startsWith("--")) {
|
|
72
|
+
return undefined
|
|
73
|
+
}
|
|
74
|
+
i++
|
|
75
|
+
return v
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
switch (flag) {
|
|
79
|
+
case "--workspace": {
|
|
80
|
+
const v = next()
|
|
81
|
+
if (v === undefined) {
|
|
82
|
+
return { options, error: "Missing value for --workspace" }
|
|
83
|
+
}
|
|
84
|
+
options.workspace = v
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
case "--checkpoint-dir": {
|
|
88
|
+
const v = next()
|
|
89
|
+
if (v === undefined) {
|
|
90
|
+
return { options, error: "Missing value for --checkpoint-dir" }
|
|
91
|
+
}
|
|
92
|
+
options.checkpointDir = v
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
case "--task-id": {
|
|
96
|
+
const v = next()
|
|
97
|
+
if (v === undefined) {
|
|
98
|
+
return { options, error: "Missing value for --task-id" }
|
|
99
|
+
}
|
|
100
|
+
options.taskId = v
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
case "--help":
|
|
104
|
+
case "-h":
|
|
105
|
+
options.help = true
|
|
106
|
+
break
|
|
107
|
+
case "list":
|
|
108
|
+
case "restore":
|
|
109
|
+
case "diff":
|
|
110
|
+
if (options.command) {
|
|
111
|
+
return { options, error: `Unexpected extra command: ${arg}` }
|
|
112
|
+
}
|
|
113
|
+
options.command = flag
|
|
114
|
+
break
|
|
115
|
+
default:
|
|
116
|
+
if (arg.startsWith("--")) {
|
|
117
|
+
return { options, error: `Unknown argument: ${arg}` }
|
|
118
|
+
}
|
|
119
|
+
options.commandArgs.push(arg)
|
|
120
|
+
break
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { options }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Find the most recently active session (taskId) for `workspace` under
|
|
129
|
+
* `checkpointDir`, by scanning `<checkpointDir>/tasks/*\/checkpoints` shadow
|
|
130
|
+
* repos and picking the one whose `core.worktree` matches and whose `.git`
|
|
131
|
+
* dir has the newest mtime.
|
|
132
|
+
*/
|
|
133
|
+
function findMostRecentTaskId(checkpointDir: string, workspace: string): string | undefined {
|
|
134
|
+
const tasksDir = path.join(checkpointDir, "tasks")
|
|
135
|
+
let entries: string[]
|
|
136
|
+
try {
|
|
137
|
+
entries = fs.readdirSync(tasksDir)
|
|
138
|
+
} catch {
|
|
139
|
+
return undefined
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const workspaceAbs = path.resolve(workspace)
|
|
143
|
+
let best: { taskId: string; mtimeMs: number } | undefined
|
|
144
|
+
|
|
145
|
+
for (const taskId of entries) {
|
|
146
|
+
const gitDir = path.join(tasksDir, taskId, "checkpoints", ".git")
|
|
147
|
+
let worktree: string
|
|
148
|
+
try {
|
|
149
|
+
worktree = execFileSync("git", ["--git-dir", gitDir, "config", "core.worktree"], {
|
|
150
|
+
encoding: "utf-8",
|
|
151
|
+
}).trim()
|
|
152
|
+
} catch {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (path.resolve(worktree) !== workspaceAbs) {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
let mtimeMs: number
|
|
159
|
+
try {
|
|
160
|
+
mtimeMs = fs.statSync(gitDir).mtimeMs
|
|
161
|
+
} catch {
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
if (!best || mtimeMs > best.mtimeMs) {
|
|
165
|
+
best = { taskId, mtimeMs }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return best?.taskId
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function resolveService(options: CheckpointsCliOptions): Promise<{ service: CheckpointService } | { error: string }> {
|
|
173
|
+
if (!options.workspace) {
|
|
174
|
+
return { error: "--workspace <path> is required" }
|
|
175
|
+
}
|
|
176
|
+
const workspace = path.resolve(options.workspace)
|
|
177
|
+
const checkpointDir = options.checkpointDir ? path.resolve(options.checkpointDir) : defaultCheckpointDir()
|
|
178
|
+
|
|
179
|
+
const taskId = options.taskId ?? findMostRecentTaskId(checkpointDir, workspace)
|
|
180
|
+
if (!taskId) {
|
|
181
|
+
return {
|
|
182
|
+
error: `No checkpoints found for workspace '${workspace}' under '${checkpointDir}'. Has a session run there yet?`,
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const service = createCheckpointService({ taskId, workspaceRoot: workspace, checkpointDir })
|
|
187
|
+
await service.init()
|
|
188
|
+
return { service }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function checkpointsMain(argv: string[]): Promise<number> {
|
|
192
|
+
const { options, error } = parseCheckpointsArgs(argv)
|
|
193
|
+
if (error) {
|
|
194
|
+
process.stderr.write(`headlesscode checkpoints: ${error}\n\n${CHECKPOINTS_USAGE}`)
|
|
195
|
+
return 2
|
|
196
|
+
}
|
|
197
|
+
if (options.help || !options.command) {
|
|
198
|
+
process.stdout.write(CHECKPOINTS_USAGE)
|
|
199
|
+
return options.help ? 0 : 2
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const resolved = await resolveService(options)
|
|
203
|
+
if ("error" in resolved) {
|
|
204
|
+
process.stderr.write(`headlesscode checkpoints: ${resolved.error}\n`)
|
|
205
|
+
return 2
|
|
206
|
+
}
|
|
207
|
+
const { service } = resolved
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
switch (options.command) {
|
|
211
|
+
case "list": {
|
|
212
|
+
const entries = await service.list()
|
|
213
|
+
if (entries.length === 0) {
|
|
214
|
+
process.stdout.write("(no checkpoints)\n")
|
|
215
|
+
return 0
|
|
216
|
+
}
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
process.stdout.write(`${entry.hash.slice(0, 12)} ${entry.date} ${entry.message}\n`)
|
|
219
|
+
}
|
|
220
|
+
return 0
|
|
221
|
+
}
|
|
222
|
+
case "restore": {
|
|
223
|
+
const hash = options.commandArgs[0]
|
|
224
|
+
if (!hash) {
|
|
225
|
+
process.stderr.write("headlesscode checkpoints restore: a commit hash is required\n")
|
|
226
|
+
return 2
|
|
227
|
+
}
|
|
228
|
+
await service.restore(hash)
|
|
229
|
+
process.stdout.write(`Restored workspace to checkpoint ${hash}\n`)
|
|
230
|
+
return 0
|
|
231
|
+
}
|
|
232
|
+
case "diff": {
|
|
233
|
+
const [from, to] = options.commandArgs
|
|
234
|
+
const changes = await service.diff({ from, to })
|
|
235
|
+
if (changes.length === 0) {
|
|
236
|
+
process.stdout.write("(no changes)\n")
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
for (const change of changes) {
|
|
240
|
+
process.stdout.write(`--- ${change.paths.relative}\n`)
|
|
241
|
+
process.stdout.write(`+++ ${change.paths.relative}\n`)
|
|
242
|
+
process.stdout.write(` before: ${change.content.before.length} chars\n`)
|
|
243
|
+
process.stdout.write(` after: ${change.content.after.length} chars\n`)
|
|
244
|
+
}
|
|
245
|
+
return 0
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} catch (err) {
|
|
249
|
+
process.stderr.write(
|
|
250
|
+
`headlesscode checkpoints ${options.command}: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
251
|
+
)
|
|
252
|
+
return 1
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return 2
|
|
256
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint service — thin wrapper around the vendored
|
|
3
|
+
* `RepoPerTaskCheckpointService` (a shadow git repo, per-task, tracking the
|
|
4
|
+
* real workspace via `core.worktree`; see
|
|
5
|
+
* `src/vendor/zoo-code/src/services/checkpoints/`).
|
|
6
|
+
*
|
|
7
|
+
* A "checkpoint" is a commit in a hidden shadow git repository — separate
|
|
8
|
+
* from any real git repo the workspace happens to have — so it never
|
|
9
|
+
* interferes with the user's own commits/branches/index. Restoring a
|
|
10
|
+
* checkpoint checks the shadow ref out onto the REAL working tree
|
|
11
|
+
* (`core.worktree` points at the workspace), so it genuinely reverts files.
|
|
12
|
+
*
|
|
13
|
+
* IMPORTANT — the shadow dir must live OUTSIDE the workspace it tracks. Zoo
|
|
14
|
+
* Code's real shadow dir is always outside any workspace (a VS Code
|
|
15
|
+
* extension's `globalStorageUri`, e.g. under the user's app-data profile).
|
|
16
|
+
* Two vendored operations assume that:
|
|
17
|
+
* - `stageAll()` (called by `initShadowGit`/`saveCheckpoint`/`getDiff`) runs
|
|
18
|
+
* `git add` with `cwd` set to the shadow dir itself. If the shadow dir is
|
|
19
|
+
* nested inside the workspace, `git add .`'s `.` pathspec resolves
|
|
20
|
+
* relative to that nested `cwd`, not the worktree root — so it only ever
|
|
21
|
+
* matches the (nearly empty) shadow dir, and both the "initial commit"
|
|
22
|
+
* and every checkpoint silently become empty-tree commits (verified
|
|
23
|
+
* empirically). This wrapper patches `stageAll` to `git add -A` (which
|
|
24
|
+
* resolves against the repo root regardless of `cwd`) as a belt-and-suspenders
|
|
25
|
+
* fix — see the patch below — but that alone does not make nesting safe:
|
|
26
|
+
* - `restoreCheckpoint()` runs `git clean -f -d -f` against the worktree.
|
|
27
|
+
* If the shadow dir is nested inside the workspace and isn't part of the
|
|
28
|
+
* shadow repo's own tracked tree (it never is — a git repo can't usefully
|
|
29
|
+
* track its own `.git`), `git clean` treats the entire shadow dir —
|
|
30
|
+
* `.git` included — as untracked cruft in the worktree and DELETES IT,
|
|
31
|
+
* destroying the checkpoint history it's mid-restore from. Verified
|
|
32
|
+
* empirically: a nested shadow dir does not survive a single restore.
|
|
33
|
+
*
|
|
34
|
+
* For both reasons, `createCheckpointService` refuses to create a service
|
|
35
|
+
* whose `checkpointDir` resolves inside `workspaceRoot`, and defaults to a
|
|
36
|
+
* per-user location outside any workspace: the central store's
|
|
37
|
+
* `~/.local/share/headlesscode/checkpoints` (mirroring Zoo Code's
|
|
38
|
+
* `globalStorageUri` design; consolidated here from the old
|
|
39
|
+
* `~/.headlesscode/checkpoints` — see src/project-store.ts's
|
|
40
|
+
* migrateCheckpointStore). Each session's shadow repo is further namespaced by
|
|
41
|
+
* a random `taskId`, so unrelated workspaces/sessions never collide there.
|
|
42
|
+
*
|
|
43
|
+
* Failure handling: every method here can throw (init can fail if git isn't
|
|
44
|
+
* installed, a nested git repo is detected, etc.). This wrapper does NOT
|
|
45
|
+
* swallow errors — callers (e.g. `HeadlessSession` in `src/engine/loop.ts`)
|
|
46
|
+
* are expected to wrap calls in try/catch and treat failures as non-fatal,
|
|
47
|
+
* exactly like the existing Phase 3 memory-recall pattern. The CLI
|
|
48
|
+
* (`src/checkpoints/cli.ts`) surfaces errors directly since a human is
|
|
49
|
+
* driving it there.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import * as path from "node:path"
|
|
53
|
+
|
|
54
|
+
import { ensureCheckpointMigration, projectStoreRoot } from "../project-store.js"
|
|
55
|
+
import { ShadowCheckpointService } from "../vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.js"
|
|
56
|
+
import type { CheckpointDiff, CheckpointResult } from "../vendor/zoo-code/src/services/checkpoints/types.js"
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default shadow-git storage root, outside any workspace (see file header).
|
|
60
|
+
* Lives under the central data store (`~/.local/share/headlesscode/checkpoints`)
|
|
61
|
+
* — consolidated from the old `~/.headlesscode/checkpoints`, which is migrated
|
|
62
|
+
* once automatically on first use (see ensureCheckpointMigration).
|
|
63
|
+
*/
|
|
64
|
+
export function defaultCheckpointDir(): string {
|
|
65
|
+
ensureCheckpointMigration()
|
|
66
|
+
return path.join(projectStoreRoot(), "checkpoints")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CreateCheckpointServiceOptions {
|
|
70
|
+
/** Session/task id — the shadow repo lives at `<checkpointDir>/tasks/<taskId>/checkpoints`. */
|
|
71
|
+
taskId: string
|
|
72
|
+
/** The real workspace whose files are being tracked (not the shadow dir itself). */
|
|
73
|
+
workspaceRoot: string
|
|
74
|
+
/** Base shadow-git storage dir. Default: `defaultCheckpointDir()`. Must NOT be inside workspaceRoot. */
|
|
75
|
+
checkpointDir?: string
|
|
76
|
+
/** Optional log sink (default: no-op — callers typically wire this to their own logger). */
|
|
77
|
+
log?: (message: string) => void
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CheckpointLogEntry {
|
|
81
|
+
hash: string
|
|
82
|
+
date: string
|
|
83
|
+
message: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when `child` is `parent` itself or nested anywhere inside it. */
|
|
87
|
+
function isSameOrInside(parent: string, child: string): boolean {
|
|
88
|
+
const parentAbs = path.resolve(parent)
|
|
89
|
+
const childAbs = path.resolve(child)
|
|
90
|
+
if (parentAbs === childAbs) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
const rel = path.relative(parentAbs, childAbs)
|
|
94
|
+
return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Internal subclass that exposes just enough of `ShadowCheckpointService`'s
|
|
99
|
+
* protected `git` handle to list commits with metadata (message/date) —
|
|
100
|
+
* `getCheckpoints()` on the base class only returns bare hashes, which isn't
|
|
101
|
+
* enough for a human-facing `checkpoints list` CLI. Everything else is
|
|
102
|
+
* inherited from the vendored class unmodified. Named "RepoPerTask..." to
|
|
103
|
+
* match the vendored subclass's directory-layout convention
|
|
104
|
+
* (`shadowDir/tasks/<taskId>/checkpoints`).
|
|
105
|
+
*/
|
|
106
|
+
class RepoPerTaskCheckpointServiceWithLog extends ShadowCheckpointService {
|
|
107
|
+
static create({ taskId, workspaceRoot, checkpointDir, log }: Required<CreateCheckpointServiceOptions>) {
|
|
108
|
+
return new RepoPerTaskCheckpointServiceWithLog(
|
|
109
|
+
taskId,
|
|
110
|
+
path.join(checkpointDir, "tasks", taskId, "checkpoints"),
|
|
111
|
+
workspaceRoot,
|
|
112
|
+
log,
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async logEntries(): Promise<CheckpointLogEntry[]> {
|
|
117
|
+
if (!this.git) {
|
|
118
|
+
throw new Error("Shadow git repo not initialized")
|
|
119
|
+
}
|
|
120
|
+
const log = await this.git.log()
|
|
121
|
+
// simple-git returns newest-first; checkpoints read more naturally oldest-first.
|
|
122
|
+
return log.all.map((c) => ({ hash: c.hash, date: c.date, message: c.message })).reverse()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Patch `ShadowCheckpointService`'s private `stageAll()` to use `git add -A`
|
|
128
|
+
* instead of the vendored `git add . --ignore-errors` — see the file header
|
|
129
|
+
* for why. `stageAll` is declared `private` on `ShadowCheckpointService`,
|
|
130
|
+
* which is a compile-time-only restriction in TypeScript; the method still
|
|
131
|
+
* exists on the prototype at runtime and can be replaced like any other.
|
|
132
|
+
* This patches ONE method's implementation on our own subclass's prototype;
|
|
133
|
+
* every other vendored code path (`initShadowGit`, `saveCheckpoint`,
|
|
134
|
+
* `restoreCheckpoint`, `getDiff`) is untouched and calls through to this
|
|
135
|
+
* patched `stageAll` exactly as before. `git add -A` behaves identically to
|
|
136
|
+
* `git add .` when `cwd` is already the repo root (the expected/enforced
|
|
137
|
+
* layout here), so this is a no-op behavior change in the supported
|
|
138
|
+
* configuration and only matters as defense in depth.
|
|
139
|
+
*/
|
|
140
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
141
|
+
;(RepoPerTaskCheckpointServiceWithLog.prototype as any).stageAll = async function (
|
|
142
|
+
this: { log: (message: string) => void },
|
|
143
|
+
git: { add: (args: string[]) => Promise<unknown> },
|
|
144
|
+
) {
|
|
145
|
+
try {
|
|
146
|
+
await git.add(["-A", "--ignore-errors"])
|
|
147
|
+
} catch (error) {
|
|
148
|
+
this.log(`[stageAll] failed to add files to git: ${error instanceof Error ? error.message : String(error)}`)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
152
|
+
|
|
153
|
+
export class CheckpointService {
|
|
154
|
+
private readonly svc: RepoPerTaskCheckpointServiceWithLog
|
|
155
|
+
private initialized = false
|
|
156
|
+
|
|
157
|
+
constructor(private readonly options: Required<CreateCheckpointServiceOptions>) {
|
|
158
|
+
this.svc = RepoPerTaskCheckpointServiceWithLog.create(options)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
get taskId(): string {
|
|
162
|
+
return this.options.taskId
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get checkpointsDir(): string {
|
|
166
|
+
return this.svc.checkpointsDir
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Initialize the shadow git repo (idempotent per process — safe to call once per session). */
|
|
170
|
+
async init(): Promise<{ created: boolean; duration: number }> {
|
|
171
|
+
if (this.initialized) {
|
|
172
|
+
return { created: false, duration: 0 }
|
|
173
|
+
}
|
|
174
|
+
const result = await this.svc.initShadowGit()
|
|
175
|
+
this.initialized = true
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Save a checkpoint (commit). Returns undefined when there was nothing to commit (no changes, allowEmpty false). */
|
|
180
|
+
async save(message: string, options?: { allowEmpty?: boolean }): Promise<CheckpointResult | undefined> {
|
|
181
|
+
return this.svc.saveCheckpoint(message, options)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Check the shadow ref out onto the real workspace, reverting files to that checkpoint. */
|
|
185
|
+
async restore(commitHash: string): Promise<void> {
|
|
186
|
+
return this.svc.restoreCheckpoint(commitHash)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Diff between two checkpoints (or a checkpoint and the current working tree when `to` is omitted). */
|
|
190
|
+
async diff(options: { from?: string; to?: string }): Promise<CheckpointDiff[]> {
|
|
191
|
+
return this.svc.getDiff(options)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** List checkpoints oldest-first, with commit message + date. */
|
|
195
|
+
async list(): Promise<CheckpointLogEntry[]> {
|
|
196
|
+
return this.svc.logEntries()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create (but do not initialize) a checkpoint service for a task/session.
|
|
202
|
+
* Call `.init()` before `.save()`/`.restore()`/`.diff()`/`.list()`.
|
|
203
|
+
*
|
|
204
|
+
* Throws synchronously if `checkpointDir` resolves inside `workspaceRoot` —
|
|
205
|
+
* see the file header for why that combination is unsafe (a restore would
|
|
206
|
+
* delete the shadow repo, and possibly nothing would ever get staged).
|
|
207
|
+
*/
|
|
208
|
+
export function createCheckpointService(options: CreateCheckpointServiceOptions): CheckpointService {
|
|
209
|
+
const checkpointDir = options.checkpointDir ?? defaultCheckpointDir()
|
|
210
|
+
|
|
211
|
+
if (isSameOrInside(options.workspaceRoot, checkpointDir)) {
|
|
212
|
+
throw new Error(
|
|
213
|
+
`Checkpoint dir '${checkpointDir}' is inside the workspace it tracks ('${options.workspaceRoot}'). ` +
|
|
214
|
+
"The shadow git repo must live outside the workspace (restoring a checkpoint runs " +
|
|
215
|
+
"`git clean` against the workspace, which would delete a nested shadow repo). " +
|
|
216
|
+
"Pass a checkpointDir outside the workspace, or omit it to use the default " +
|
|
217
|
+
`(${defaultCheckpointDir()}).`,
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return new CheckpointService({
|
|
222
|
+
taskId: options.taskId,
|
|
223
|
+
workspaceRoot: options.workspaceRoot,
|
|
224
|
+
checkpointDir,
|
|
225
|
+
log: options.log ?? (() => {}),
|
|
226
|
+
})
|
|
227
|
+
}
|