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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode dashboard` subcommand — a local, auto-refreshing cost/token
|
|
3
|
+
* dashboard (workstream 3) + live per-session event feed + pause/resume
|
|
4
|
+
* control (live worker monitoring).
|
|
5
|
+
*
|
|
6
|
+
* npx tsx src/cli.ts dashboard [--port 4390] [--repo <path>]
|
|
7
|
+
*
|
|
8
|
+
* Thin dispatch to src/dashboard/server.ts — no logic duplicated here. When
|
|
9
|
+
* `--repo` is omitted the page still starts but shows no sessions/round data
|
|
10
|
+
* (the aggregation gracefully degrades — see src/dashboard/aggregate.ts).
|
|
11
|
+
*
|
|
12
|
+
* NOTE (Phase 3 scope change): the dashboard is no longer purely read-only —
|
|
13
|
+
* the page's pause/resume buttons issue state-changing POSTs to the running
|
|
14
|
+
* worker (see src/dashboard/server.ts's header note). Still local-only +
|
|
15
|
+
* no-auth (binds 127.0.0.1), but that assumption needs revisiting if this is
|
|
16
|
+
* ever exposed beyond localhost.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { DEFAULT_DASHBOARD_PORT, startDashboardServer } from "./server.js"
|
|
20
|
+
|
|
21
|
+
const DASHBOARD_USAGE = `headlesscode dashboard — local cost/token dashboard + live worker control
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
headlesscode dashboard [--port 4390] [--repo <path>]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--port <n> Port to listen on (default: 4390, or $HEADLESSCODE_DASHBOARD_PORT)
|
|
28
|
+
--repo <path> Repo root to scan for .headlesscode/usage/*.jsonl (own +
|
|
29
|
+
.worktrees/*) and .worktrees/.orchestrator-state.json.
|
|
30
|
+
Omit to start the page with no data (still useful to confirm
|
|
31
|
+
the server runs).
|
|
32
|
+
--help Show this help and exit
|
|
33
|
+
|
|
34
|
+
Environment:
|
|
35
|
+
HEADLESSCODE_DASHBOARD_TOKEN Optional bearer token for the control-plane
|
|
36
|
+
POST routes (/api/session/start|pause|resume|answer) and
|
|
37
|
+
the destructive /api/checkpoints/restore. Unset (default) =
|
|
38
|
+
no auth. Local-only tool; see the notes below.
|
|
39
|
+
HEADLESSCODE_DASHBOARD_SESSION_CLI Override the CLI invocation
|
|
40
|
+
POST /api/session/start spawns (default: "npx tsx
|
|
41
|
+
src/cli.ts", resolved from HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT).
|
|
42
|
+
Not a CLI flag deliberately (DashboardServerOptions.cli is a
|
|
43
|
+
test-only injection point) — this exists for container
|
|
44
|
+
deployments where the dashboard runs from a live bind-mounted
|
|
45
|
+
source tree but "npx tsx" has no node_modules there to
|
|
46
|
+
resolve against (see docker-compose.headlesscode-local.yml).
|
|
47
|
+
HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT Override the directory a launched
|
|
48
|
+
session's CLI runs in (default: process.cwd()). Same
|
|
49
|
+
container rationale as above — the dashboard's own cwd may
|
|
50
|
+
be a baked image copy while the live source lives elsewhere.
|
|
51
|
+
HEADLESSCODE_DASHBOARD_WORKTREE_ROOT Root directory for POST
|
|
52
|
+
/api/projects/ensure-worktree's disposable per-project
|
|
53
|
+
worktrees. Unset (default) = that endpoint responds 501.
|
|
54
|
+
HEADLESSCODE_DASHBOARD_WORKTREE_OWNER chown target (e.g. "1000:1000")
|
|
55
|
+
for a newly-created worktree and the ref/admin files
|
|
56
|
+
'git worktree add' writes into the source repo's own
|
|
57
|
+
.git — read directly by session-launch.ts, not a
|
|
58
|
+
startDashboardServer option. Only matters when this
|
|
59
|
+
process runs as root (a container's usual default);
|
|
60
|
+
unset (default) = skip, leaving those files owned by
|
|
61
|
+
whoever ran the dashboard.
|
|
62
|
+
|
|
63
|
+
Notes:
|
|
64
|
+
Binds to 127.0.0.1 only — no auth unless HEADLESSCODE_DASHBOARD_TOKEN is
|
|
65
|
+
set. Never expose this port beyond localhost.
|
|
66
|
+
IMPORTANT (browser control plane): the "start a session" endpoint spawns
|
|
67
|
+
the CLI as a detached child that INHERITS this process's environment — it
|
|
68
|
+
does NOT re-source .env. Export HEADLESSCODE_OPENROUTER_API_KEY (e.g. 'set -a; source
|
|
69
|
+
.env; set +a') before starting the dashboard, or launched sessions will
|
|
70
|
+
fail immediately with an OpenRouter auth error.
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
interface DashboardCliOptions {
|
|
74
|
+
port: number
|
|
75
|
+
repo?: string
|
|
76
|
+
help: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseDashboardArgs(argv: string[]): { options: DashboardCliOptions; error?: string } {
|
|
80
|
+
const options: DashboardCliOptions = {
|
|
81
|
+
port: Number(process.env.HEADLESSCODE_DASHBOARD_PORT ?? DEFAULT_DASHBOARD_PORT),
|
|
82
|
+
help: false,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (let i = 0; i < argv.length; i++) {
|
|
86
|
+
const arg = argv[i]
|
|
87
|
+
const eq = arg.indexOf("=")
|
|
88
|
+
const flag = eq === -1 ? arg : arg.slice(0, eq)
|
|
89
|
+
const inlineValue = eq === -1 ? undefined : arg.slice(eq + 1)
|
|
90
|
+
const next = (): string | undefined => {
|
|
91
|
+
if (inlineValue !== undefined) {
|
|
92
|
+
return inlineValue
|
|
93
|
+
}
|
|
94
|
+
const v = argv[i + 1]
|
|
95
|
+
if (v === undefined || v.startsWith("--")) {
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
i++
|
|
99
|
+
return v
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
switch (flag) {
|
|
103
|
+
case "--port": {
|
|
104
|
+
const v = next()
|
|
105
|
+
const n = v === undefined ? Number.NaN : Number(v)
|
|
106
|
+
if (!Number.isInteger(n) || n <= 0 || n > 65535) {
|
|
107
|
+
return { options, error: "--port requires an integer between 1 and 65535" }
|
|
108
|
+
}
|
|
109
|
+
options.port = n
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
case "--repo": {
|
|
113
|
+
const v = next()
|
|
114
|
+
if (v === undefined) {
|
|
115
|
+
return { options, error: "Missing value for --repo" }
|
|
116
|
+
}
|
|
117
|
+
options.repo = v
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
case "--help":
|
|
121
|
+
case "-h":
|
|
122
|
+
options.help = true
|
|
123
|
+
break
|
|
124
|
+
default:
|
|
125
|
+
return { options, error: `Unknown argument: ${arg}` }
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { options }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function dashboardMain(argv: string[]): Promise<number> {
|
|
133
|
+
const { options, error } = parseDashboardArgs(argv)
|
|
134
|
+
if (error) {
|
|
135
|
+
process.stderr.write(`headlesscode dashboard: ${error}\n\n${DASHBOARD_USAGE}`)
|
|
136
|
+
return 2
|
|
137
|
+
}
|
|
138
|
+
if (options.help) {
|
|
139
|
+
process.stdout.write(DASHBOARD_USAGE)
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let server: Awaited<ReturnType<typeof startDashboardServer>>
|
|
144
|
+
try {
|
|
145
|
+
server = await startDashboardServer({
|
|
146
|
+
port: options.port,
|
|
147
|
+
repo: options.repo,
|
|
148
|
+
token: process.env.HEADLESSCODE_DASHBOARD_TOKEN || undefined,
|
|
149
|
+
cli: process.env.HEADLESSCODE_DASHBOARD_SESSION_CLI || undefined,
|
|
150
|
+
repoRoot: process.env.HEADLESSCODE_DASHBOARD_SESSION_REPO_ROOT || undefined,
|
|
151
|
+
worktreeRoot: process.env.HEADLESSCODE_DASHBOARD_WORKTREE_ROOT || undefined,
|
|
152
|
+
})
|
|
153
|
+
} catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
155
|
+
if ((err as NodeJS.ErrnoException)?.code === "EADDRINUSE") {
|
|
156
|
+
process.stderr.write(
|
|
157
|
+
`headlesscode dashboard: port ${options.port} is already in use. Pick another with --port <n>.\n`,
|
|
158
|
+
)
|
|
159
|
+
return 2
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(`headlesscode dashboard: failed to start: ${message}\n`)
|
|
162
|
+
return 2
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
process.stdout.write(
|
|
166
|
+
`[dashboard] listening on http://127.0.0.1:${options.port}${options.repo ? ` (repo: ${options.repo})` : " (no --repo given; page will show no data)"}\n`,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Keep the process alive until interrupted; clean shutdown on SIGINT/SIGTERM.
|
|
170
|
+
return new Promise<number>((resolve) => {
|
|
171
|
+
let shuttingDown = false
|
|
172
|
+
const onSignal = (): void => {
|
|
173
|
+
// Only the first signal shuts down; a second Ctrl-C while the
|
|
174
|
+
// server is mid-close is ignored (the process exits on its own
|
|
175
|
+
// once the listening handle is gone).
|
|
176
|
+
if (shuttingDown) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
shuttingDown = true
|
|
180
|
+
process.stdout.write("\n[dashboard] shutting down\n")
|
|
181
|
+
process.removeListener("SIGINT", onSignal)
|
|
182
|
+
process.removeListener("SIGTERM", onSignal)
|
|
183
|
+
// Release the port: stop accepting new connections and drop
|
|
184
|
+
// keep-alive sockets so the process can exit. Without close the
|
|
185
|
+
// listening handle keeps the event loop alive and the port bound.
|
|
186
|
+
server.close()
|
|
187
|
+
server.closeAllConnections?.()
|
|
188
|
+
resolve(0)
|
|
189
|
+
}
|
|
190
|
+
process.on("SIGINT", onSignal)
|
|
191
|
+
process.on("SIGTERM", onSignal)
|
|
192
|
+
})
|
|
193
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard read-side for the codemap: serves the stored codemap.json /
|
|
3
|
+
* codemap.html from the CENTRAL per-project store (same resolution the
|
|
4
|
+
* `headlesscode codemap` CLI writes to — see src/codemap/lock.ts). The
|
|
5
|
+
* dashboard deliberately does NOT generate the map on request (it's a
|
|
6
|
+
* deterministic script job, run by the CLI/watcher); it just serves what's
|
|
7
|
+
* there and gives a clear pointer when nothing has been generated yet.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "node:fs"
|
|
11
|
+
|
|
12
|
+
import { codemapHtmlPath, codemapJsonPath } from "../codemap/lock.js"
|
|
13
|
+
|
|
14
|
+
export interface CodemapFileResult {
|
|
15
|
+
content: string
|
|
16
|
+
contentType: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The stored codemap.json content; undefined when not generated yet. */
|
|
20
|
+
export function readCodemapJson(workspaceRoot: string): CodemapFileResult | undefined {
|
|
21
|
+
return readFileIfPresent(codemapJsonPath(workspaceRoot), "application/json; charset=utf-8")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The stored codemap.html content; undefined when not generated yet. */
|
|
25
|
+
export function readCodemapHtml(workspaceRoot: string): CodemapFileResult | undefined {
|
|
26
|
+
return readFileIfPresent(codemapHtmlPath(workspaceRoot), "text/html; charset=utf-8")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The error message + exit hint a route returns when the map doesn't exist yet. */
|
|
30
|
+
export function codemapMissingError(workspaceRoot: string): string {
|
|
31
|
+
return (
|
|
32
|
+
`no codemap generated for ${workspaceRoot} yet — run ` +
|
|
33
|
+
`\`headlesscode codemap --workspace ${workspaceRoot}\` (or start it in --watch mode) to build it.`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readFileIfPresent(file: string, contentType: string): CodemapFileResult | undefined {
|
|
38
|
+
try {
|
|
39
|
+
const content = fs.readFileSync(file, "utf-8")
|
|
40
|
+
return { content, contentType }
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard file-browser backend — thin adapters over the tool executor's
|
|
3
|
+
* existing, already-tested directory-listing logic.
|
|
4
|
+
*
|
|
5
|
+
* The non-negotiable rule here is path safety: every path that crosses the
|
|
6
|
+
* HTTP surface goes through `resolveWithinWorkspace` / `PathTraversalError`
|
|
7
|
+
* (src/tools/executor.ts) — the SAME guard the model-facing tools use. A bug
|
|
8
|
+
* here would be a real directory-traversal vulnerability on a locally-running
|
|
9
|
+
* HTTP server, so this module deliberately reuses that guard instead of
|
|
10
|
+
* reimplementing containment checking.
|
|
11
|
+
*
|
|
12
|
+
* Listing semantics mirror `list_files` (`listFilesHandler` in
|
|
13
|
+
* src/tools/executor.ts): a raw `readdir` of the requested directory, no
|
|
14
|
+
* `.gitignore` filtering (the model-facing tool doesn't filter either — the
|
|
15
|
+
* gitignore-aware walker lives in src/codesearch/files.ts for the codebase
|
|
16
|
+
* index, a different surface). Each entry carries its name, type, and size so
|
|
17
|
+
* the browser can render a tree without extra round-trips.
|
|
18
|
+
*
|
|
19
|
+
* Content retrieval mirrors `read_file`'s size discipline: files over
|
|
20
|
+
* `MAX_RESULT_CHARS` (30k chars, the same cap tool results use to keep model
|
|
21
|
+
* context bounded) or containing NUL bytes (a strong binary signal) return a
|
|
22
|
+
* clear "not previewable" response instead of dumping raw bytes into a JSON
|
|
23
|
+
* response.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fsp from "node:fs/promises"
|
|
27
|
+
import * as path from "node:path"
|
|
28
|
+
|
|
29
|
+
import { resolveWithinWorkspace, PathTraversalError, MAX_RESULT_CHARS } from "../tools/executor.js"
|
|
30
|
+
import { DEFAULT_PROTECTED_FILES, isProtectedPath } from "../permissions/protected-files.js"
|
|
31
|
+
|
|
32
|
+
/** One directory entry, ready for JSON. */
|
|
33
|
+
export interface FileEntry {
|
|
34
|
+
/** Entry name (basename) — NOT a path, safe to render directly. */
|
|
35
|
+
name: string
|
|
36
|
+
type: "file" | "dir"
|
|
37
|
+
/** Size in bytes (0 for directories). */
|
|
38
|
+
size: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List a directory's entries for a workspace. `dir` is a workspace-relative
|
|
43
|
+
* path (default "."). Throws `PathTraversalError` when the requested path
|
|
44
|
+
* escapes the workspace — callers (the HTTP layer) translate that to a 400.
|
|
45
|
+
*/
|
|
46
|
+
export async function listWorkspaceDir(workspaceRoot: string, dir: string): Promise<FileEntry[]> {
|
|
47
|
+
const root = path.resolve(workspaceRoot)
|
|
48
|
+
const target = resolveWithinWorkspace(root, dir)
|
|
49
|
+
|
|
50
|
+
const dirents = await fsp.readdir(target, { withFileTypes: true })
|
|
51
|
+
const out: FileEntry[] = []
|
|
52
|
+
for (const ent of dirents) {
|
|
53
|
+
const abs = path.join(target, ent.name)
|
|
54
|
+
let size = 0
|
|
55
|
+
if (ent.isFile()) {
|
|
56
|
+
try {
|
|
57
|
+
const st = await fsp.stat(abs)
|
|
58
|
+
size = st.size
|
|
59
|
+
} catch {
|
|
60
|
+
// File vanished between readdir and stat — report size 0.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
out.push({ name: ent.name, type: ent.isDirectory() ? "dir" : "file", size })
|
|
64
|
+
}
|
|
65
|
+
// Dirs first, then alphabetical — same ordering list_files uses.
|
|
66
|
+
out.sort((a, b) => {
|
|
67
|
+
if (a.type !== b.type) {
|
|
68
|
+
return a.type === "dir" ? -1 : 1
|
|
69
|
+
}
|
|
70
|
+
return a.name.localeCompare(b.name)
|
|
71
|
+
})
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Result of a file-content read: previewable text, or a clear refusal. */
|
|
76
|
+
export type FileContentResult =
|
|
77
|
+
| { previewable: true; content: string }
|
|
78
|
+
| { previewable: false; reason: string }
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Read a single file's content for browser preview. Only text files are
|
|
82
|
+
* previewable: content over `MAX_RESULT_CHARS` or containing NUL bytes (the
|
|
83
|
+
* strongest cheap binary signal) returns a "not previewable" reason instead
|
|
84
|
+
* of raw bytes. Throws `PathTraversalError` for escaping paths.
|
|
85
|
+
*
|
|
86
|
+
* SEC-7: files matching the protected-file patterns (`.env`, `*.pem`, `*.key`,
|
|
87
|
+
* `id_rsa*`, etc. — the same defaults `src/permissions/protected-files.ts`
|
|
88
|
+
* uses to stop the WRITE tools from touching secrets) are refused here too,
|
|
89
|
+
* unconditionally — the dashboard's GET file-content route must not become a
|
|
90
|
+
* read side-channel for the exact files the write-side guard protects. This
|
|
91
|
+
* check is independent of the optional bearer-token gate in server.ts (it
|
|
92
|
+
* applies even when no token is configured).
|
|
93
|
+
*/
|
|
94
|
+
export async function readWorkspaceFile(workspaceRoot: string, file: string): Promise<FileContentResult> {
|
|
95
|
+
const root = path.resolve(workspaceRoot)
|
|
96
|
+
const target = resolveWithinWorkspace(root, file)
|
|
97
|
+
|
|
98
|
+
const relFromRoot = path.relative(root, target).replace(/\\/g, "/")
|
|
99
|
+
if (isProtectedPath(relFromRoot, DEFAULT_PROTECTED_FILES)) {
|
|
100
|
+
return { previewable: false, reason: "protected file — not previewable via the dashboard (matches a secrets pattern like .env/*.key/*.pem)" }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const st = await fsp.stat(target)
|
|
104
|
+
if (!st.isFile()) {
|
|
105
|
+
return { previewable: false, reason: "not a file" }
|
|
106
|
+
}
|
|
107
|
+
if (st.size > MAX_RESULT_CHARS) {
|
|
108
|
+
return {
|
|
109
|
+
previewable: false,
|
|
110
|
+
reason: `file is ${st.size.toLocaleString()} bytes — over the ${MAX_RESULT_CHARS.toLocaleString()}-char preview cap (same convention as read_file's MAX_RESULT_CHARS)`,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const buf = await fsp.readFile(target)
|
|
115
|
+
if (buf.includes(0)) {
|
|
116
|
+
return { previewable: false, reason: "binary file — contains NUL bytes, not text-previewable" }
|
|
117
|
+
}
|
|
118
|
+
// UTF-8 decode; replacement chars for invalid sequences are acceptable
|
|
119
|
+
// here (the NUL check above already caught the common binary case).
|
|
120
|
+
return { previewable: true, content: buf.toString("utf-8") }
|
|
121
|
+
}
|