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,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `headlesscode projects` subcommand — enumerate and prune the central
|
|
3
|
+
* per-project store (plans/project-registry-and-store-cleanup.md).
|
|
4
|
+
*
|
|
5
|
+
* headlesscode projects list [--json] [--registered-only] [--stale] [--size]
|
|
6
|
+
* headlesscode projects prune [--dry-run] [--yes] [--include-registered]
|
|
7
|
+
*
|
|
8
|
+
* Read side (`list`) and reclaim side (`prune`) over the same underlying
|
|
9
|
+
* listing function (`listProjectEntries` in src/project-store.ts) the
|
|
10
|
+
* dashboard's GET /api/projects endpoint reuses — one source of truth for
|
|
11
|
+
* "what's in the store".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import * as fs from "node:fs"
|
|
15
|
+
import * as path from "node:path"
|
|
16
|
+
|
|
17
|
+
import { isUnderSystemTmpDir, listProjectEntries, projectStoreRoot, type ProjectListEntry } from "../project-store.js"
|
|
18
|
+
|
|
19
|
+
const PROJECTS_USAGE = `headlesscode projects — enumerate and prune the central per-project store
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
headlesscode projects list [--json] [--registered-only] [--stale] [--size]
|
|
23
|
+
headlesscode projects prune [--dry-run] [--yes] [--include-registered]
|
|
24
|
+
|
|
25
|
+
The central store lives at ~/.local/share/headlesscode/projects/<key>/ (see
|
|
26
|
+
src/project-store.ts). Each directory holds one project's cross-session data
|
|
27
|
+
(codesearch index, mode-models.json, permissions.json) plus a project.json
|
|
28
|
+
metadata file recording the real path, kind, first/last-seen and whether a
|
|
29
|
+
human deliberately registered the project via \`headlesscode init\`.
|
|
30
|
+
|
|
31
|
+
list:
|
|
32
|
+
A compact PROJECT / SIZE / PATH table, sorted by on-disk size (largest
|
|
33
|
+
first), with a one-line summary of totals. By default the table hides
|
|
34
|
+
/tmp test-workspace clutter and no-project.json litter (use --all to see
|
|
35
|
+
everything, with the fuller KEY/REG/OK/KIND columns). Sizes are computed
|
|
36
|
+
only for the entries actually printed (after filters), never for the
|
|
37
|
+
whole store.
|
|
38
|
+
--all Show every entry, including /tmp litter, with the
|
|
39
|
+
fuller KEY/REG/OK/KIND/PATH/FIRST SEEN/LAST SEEN table
|
|
40
|
+
--json Print a JSON array of every entry (unfiltered)
|
|
41
|
+
instead of the table, sorted by lastSeen (no size
|
|
42
|
+
field, no summary line — stdout is pure JSON)
|
|
43
|
+
--registered-only Only projects a human registered via \`headlesscode init\`
|
|
44
|
+
--stale Only projects whose recorded path no longer exists
|
|
45
|
+
--size No-op (size is always shown); kept for backward
|
|
46
|
+
compatibility
|
|
47
|
+
|
|
48
|
+
prune:
|
|
49
|
+
Remove store entries that are safe to reclaim. Default candidates: entries
|
|
50
|
+
whose recorded path no longer exists AND that were never registered, plus
|
|
51
|
+
directories with no project.json at all (the pre-registry litter Part A
|
|
52
|
+
stops producing — they were never a curated project to begin with). A
|
|
53
|
+
registered project is NEVER removed by default, even when its path is
|
|
54
|
+
missing.
|
|
55
|
+
--dry-run Print what would be removed and the bytes reclaimed,
|
|
56
|
+
remove nothing, exit 0
|
|
57
|
+
--include-registered Also include registered-but-missing-path entries — the
|
|
58
|
+
escape hatch for a deleted repo or a temporarily
|
|
59
|
+
unmounted drive (you can't tell which from here)
|
|
60
|
+
--yes Actually remove the candidates. Without it (and without
|
|
61
|
+
--dry-run) the candidate list + byte total are printed
|
|
62
|
+
and the command exits 2 asking you to re-run with --yes
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
--help Show this help and exit
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
export interface ProjectsCliOptions {
|
|
69
|
+
subcommand?: "list" | "prune"
|
|
70
|
+
json: boolean
|
|
71
|
+
registeredOnly: boolean
|
|
72
|
+
stale: boolean
|
|
73
|
+
size: boolean
|
|
74
|
+
all: boolean
|
|
75
|
+
dryRun: boolean
|
|
76
|
+
yes: boolean
|
|
77
|
+
includeRegistered: boolean
|
|
78
|
+
help: boolean
|
|
79
|
+
error?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function parseProjectsArgs(argv: string[]): ProjectsCliOptions {
|
|
83
|
+
const options: ProjectsCliOptions = {
|
|
84
|
+
json: false,
|
|
85
|
+
registeredOnly: false,
|
|
86
|
+
stale: false,
|
|
87
|
+
size: false,
|
|
88
|
+
all: false,
|
|
89
|
+
dryRun: false,
|
|
90
|
+
yes: false,
|
|
91
|
+
includeRegistered: false,
|
|
92
|
+
help: false,
|
|
93
|
+
}
|
|
94
|
+
const [subcommand, ...rest] = argv
|
|
95
|
+
if (subcommand === undefined || subcommand === "--help" || subcommand === "-h") {
|
|
96
|
+
options.help = true
|
|
97
|
+
return options
|
|
98
|
+
}
|
|
99
|
+
if (subcommand === "list") {
|
|
100
|
+
options.subcommand = "list"
|
|
101
|
+
} else if (subcommand === "prune") {
|
|
102
|
+
options.subcommand = "prune"
|
|
103
|
+
} else {
|
|
104
|
+
return { ...options, error: `Unknown subcommand: ${subcommand}` }
|
|
105
|
+
}
|
|
106
|
+
for (const arg of rest) {
|
|
107
|
+
switch (arg) {
|
|
108
|
+
case "--help":
|
|
109
|
+
case "-h":
|
|
110
|
+
options.help = true
|
|
111
|
+
break
|
|
112
|
+
case "--json":
|
|
113
|
+
if (options.subcommand !== "list") {
|
|
114
|
+
return { ...options, error: "--json is only valid with `projects list`" }
|
|
115
|
+
}
|
|
116
|
+
options.json = true
|
|
117
|
+
break
|
|
118
|
+
case "--registered-only":
|
|
119
|
+
if (options.subcommand !== "list") {
|
|
120
|
+
return { ...options, error: "--registered-only is only valid with `projects list`" }
|
|
121
|
+
}
|
|
122
|
+
options.registeredOnly = true
|
|
123
|
+
break
|
|
124
|
+
case "--stale":
|
|
125
|
+
if (options.subcommand !== "list") {
|
|
126
|
+
return { ...options, error: "--stale is only valid with `projects list`" }
|
|
127
|
+
}
|
|
128
|
+
options.stale = true
|
|
129
|
+
break
|
|
130
|
+
case "--size":
|
|
131
|
+
if (options.subcommand !== "list") {
|
|
132
|
+
return { ...options, error: "--size is only valid with `projects list`" }
|
|
133
|
+
}
|
|
134
|
+
options.size = true
|
|
135
|
+
break
|
|
136
|
+
case "--all":
|
|
137
|
+
if (options.subcommand !== "list") {
|
|
138
|
+
return { ...options, error: "--all is only valid with `projects list`" }
|
|
139
|
+
}
|
|
140
|
+
options.all = true
|
|
141
|
+
break
|
|
142
|
+
case "--dry-run":
|
|
143
|
+
if (options.subcommand !== "prune") {
|
|
144
|
+
return { ...options, error: "--dry-run is only valid with `projects prune`" }
|
|
145
|
+
}
|
|
146
|
+
options.dryRun = true
|
|
147
|
+
break
|
|
148
|
+
case "--yes":
|
|
149
|
+
if (options.subcommand !== "prune") {
|
|
150
|
+
return { ...options, error: "--yes is only valid with `projects prune`" }
|
|
151
|
+
}
|
|
152
|
+
options.yes = true
|
|
153
|
+
break
|
|
154
|
+
case "--include-registered":
|
|
155
|
+
if (options.subcommand !== "prune") {
|
|
156
|
+
return { ...options, error: "--include-registered is only valid with `projects prune`" }
|
|
157
|
+
}
|
|
158
|
+
options.includeRegistered = true
|
|
159
|
+
break
|
|
160
|
+
default:
|
|
161
|
+
return { ...options, error: `Unknown argument: ${arg}` }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return options
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Sort by lastSeen descending; entries with no (or malformed) lastSeen sort last. */
|
|
168
|
+
function sortEntries(entries: ProjectListEntry[]): ProjectListEntry[] {
|
|
169
|
+
return [...entries].sort((a, b) => {
|
|
170
|
+
const at = a.lastSeen === undefined ? Number.NaN : Date.parse(a.lastSeen)
|
|
171
|
+
const bt = b.lastSeen === undefined ? Number.NaN : Date.parse(b.lastSeen)
|
|
172
|
+
const aValid = !Number.isNaN(at)
|
|
173
|
+
const bValid = !Number.isNaN(bt)
|
|
174
|
+
if (!aValid && !bValid) return 0
|
|
175
|
+
if (!aValid) return 1
|
|
176
|
+
if (!bValid) return -1
|
|
177
|
+
return bt - at
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Recursive byte size of a directory (used only for --size / prune totals). */
|
|
182
|
+
function directorySize(dir: string): number {
|
|
183
|
+
let total = 0
|
|
184
|
+
let entries: string[]
|
|
185
|
+
try {
|
|
186
|
+
entries = fs.readdirSync(dir)
|
|
187
|
+
} catch {
|
|
188
|
+
return 0
|
|
189
|
+
}
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const full = path.join(dir, entry)
|
|
192
|
+
let st: fs.Stats
|
|
193
|
+
try {
|
|
194
|
+
st = fs.lstatSync(full)
|
|
195
|
+
} catch {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (st.isDirectory()) {
|
|
199
|
+
total += directorySize(full)
|
|
200
|
+
} else if (st.isFile()) {
|
|
201
|
+
total += st.size
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return total
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function humanSize(bytes: number): string {
|
|
208
|
+
if (bytes < 1024) return `${bytes} B`
|
|
209
|
+
let value = bytes / 1024
|
|
210
|
+
let unit = "KB"
|
|
211
|
+
for (const next of ["MB", "GB", "TB"]) {
|
|
212
|
+
if (value < 1024) break
|
|
213
|
+
value /= 1024
|
|
214
|
+
unit = next
|
|
215
|
+
}
|
|
216
|
+
return `${value.toFixed(1)} ${unit}`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Right-align a column of numbers in a table (e.g. the size column). */
|
|
220
|
+
function padStartCells(rows: string[][], col: number): void {
|
|
221
|
+
const width = rows.reduce((w, row) => Math.max(w, row[col].length), 0)
|
|
222
|
+
for (const row of rows) {
|
|
223
|
+
row[col] = row[col].padStart(width)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatTable(rows: string[][]): string {
|
|
228
|
+
const widths: number[] = []
|
|
229
|
+
for (const row of rows) {
|
|
230
|
+
row.forEach((cell, i) => {
|
|
231
|
+
widths[i] = Math.max(widths[i] ?? 0, cell.length)
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
return rows
|
|
235
|
+
.map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd())
|
|
236
|
+
.join("\n")
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** /tmp test-workspace clutter and no-project.json litter — hidden from the default table. */
|
|
240
|
+
function isClutter(e: ProjectListEntry): boolean {
|
|
241
|
+
return e.path === undefined || isUnderSystemTmpDir(e.path)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function listMain(options: ProjectsCliOptions): Promise<number> {
|
|
245
|
+
let entries = sortEntries(listProjectEntries())
|
|
246
|
+
if (options.registeredOnly) {
|
|
247
|
+
entries = entries.filter((e) => e.registered)
|
|
248
|
+
}
|
|
249
|
+
if (options.stale) {
|
|
250
|
+
entries = entries.filter((e) => !e.exists)
|
|
251
|
+
}
|
|
252
|
+
if (options.json) {
|
|
253
|
+
process.stdout.write(JSON.stringify(entries, null, 2) + "\n")
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const totalCount = entries.length
|
|
258
|
+
const hiddenCount = options.all ? 0 : entries.filter(isClutter).length
|
|
259
|
+
if (!options.all) {
|
|
260
|
+
entries = entries.filter((e) => !isClutter(e))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Size is computed only for the entries actually being printed (after
|
|
264
|
+
// filters) — never for the whole polluted store — and entries sort by
|
|
265
|
+
// size descending, so the projects that actually matter (real, existing,
|
|
266
|
+
// sizeable) surface first. --size is kept as an accepted (now no-op)
|
|
267
|
+
// flag for backward compatibility.
|
|
268
|
+
const sizeBytesByKey = new Map<string, number>()
|
|
269
|
+
for (const e of entries) {
|
|
270
|
+
sizeBytesByKey.set(e.key, directorySize(path.join(projectStoreRoot(), "projects", e.key)))
|
|
271
|
+
}
|
|
272
|
+
entries = [...entries].sort((a, b) => (sizeBytesByKey.get(b.key) ?? 0) - (sizeBytesByKey.get(a.key) ?? 0))
|
|
273
|
+
|
|
274
|
+
const rows: string[][] = options.all
|
|
275
|
+
? [
|
|
276
|
+
["KEY", "REG", "OK", "KIND", "SIZE", "PATH", "FIRST SEEN", "LAST SEEN"],
|
|
277
|
+
...entries.map((e) => [
|
|
278
|
+
e.key,
|
|
279
|
+
e.registered ? "✓" : "·",
|
|
280
|
+
e.exists ? "ok" : "gone",
|
|
281
|
+
e.kind ?? "-",
|
|
282
|
+
humanSize(sizeBytesByKey.get(e.key) ?? 0),
|
|
283
|
+
e.path ?? "-",
|
|
284
|
+
e.firstSeen ?? "-",
|
|
285
|
+
e.lastSeen ?? "-",
|
|
286
|
+
]),
|
|
287
|
+
]
|
|
288
|
+
: [
|
|
289
|
+
["PROJECT", "SIZE", "PATH"],
|
|
290
|
+
...entries.map((e) => [
|
|
291
|
+
path.basename(e.path ?? e.key),
|
|
292
|
+
humanSize(sizeBytesByKey.get(e.key) ?? 0),
|
|
293
|
+
e.path ?? "-",
|
|
294
|
+
]),
|
|
295
|
+
]
|
|
296
|
+
padStartCells(rows, options.all ? 4 : 1)
|
|
297
|
+
process.stdout.write(formatTable(rows) + "\n")
|
|
298
|
+
|
|
299
|
+
const registeredCount = entries.filter((e) => e.registered).length
|
|
300
|
+
const missingCount = entries.filter((e) => !e.exists).length
|
|
301
|
+
process.stdout.write(`\n${entries.length} project(s): ${registeredCount} registered, ${missingCount} with missing paths`)
|
|
302
|
+
process.stdout.write(
|
|
303
|
+
hiddenCount > 0
|
|
304
|
+
? ` (${hiddenCount} /tmp or no-metadata entr${hiddenCount === 1 ? "y" : "ies"} hidden — use --all to show, ${totalCount} total)\n`
|
|
305
|
+
: "\n",
|
|
306
|
+
)
|
|
307
|
+
return 0
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function pruneMain(options: ProjectsCliOptions): Promise<number> {
|
|
311
|
+
// Default candidates: exists === false && registered === false. An entry
|
|
312
|
+
// with NO project.json at all is path=undefined → exists=false →
|
|
313
|
+
// registered=false, so it's already captured — the "no project.json" class
|
|
314
|
+
// from the plan is exactly this predicate, not a separate case.
|
|
315
|
+
const candidates = sortEntries(listProjectEntries())
|
|
316
|
+
.filter((e) => !e.exists && (options.includeRegistered || !e.registered))
|
|
317
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
318
|
+
|
|
319
|
+
let totalBytes = 0
|
|
320
|
+
const details = candidates.map((e) => {
|
|
321
|
+
const size = directorySize(path.join(projectStoreRoot(), "projects", e.key))
|
|
322
|
+
totalBytes += size
|
|
323
|
+
return { entry: e, size }
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
if (details.length === 0) {
|
|
327
|
+
process.stdout.write("projects prune: nothing to remove\n")
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
process.stdout.write("Candidates for removal:\n")
|
|
332
|
+
for (const { entry, size } of details) {
|
|
333
|
+
process.stdout.write(
|
|
334
|
+
` ${entry.key} ${humanSize(size).padStart(10)} ${entry.path ?? "(no project.json)"}\n`,
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
process.stdout.write(
|
|
338
|
+
`\nTotal reclaimable: ${humanSize(totalBytes)} (${totalBytes} bytes) across ${details.length} entr${
|
|
339
|
+
details.length === 1 ? "y" : "ies"
|
|
340
|
+
}\n`,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if (options.dryRun) {
|
|
344
|
+
process.stdout.write("Dry run — nothing removed.\n")
|
|
345
|
+
return 0
|
|
346
|
+
}
|
|
347
|
+
if (!options.yes) {
|
|
348
|
+
process.stderr.write("\nRe-run with --yes to actually remove these store entries.\n")
|
|
349
|
+
return 2
|
|
350
|
+
}
|
|
351
|
+
for (const { entry } of details) {
|
|
352
|
+
const dir = path.join(projectStoreRoot(), "projects", entry.key)
|
|
353
|
+
try {
|
|
354
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
355
|
+
process.stdout.write(` removed ${entry.key} (${entry.path ?? "no project.json"})\n`)
|
|
356
|
+
} catch (err) {
|
|
357
|
+
process.stderr.write(` FAILED to remove ${entry.key}: ${err instanceof Error ? err.message : String(err)}\n`)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
process.stdout.write(`\nRemoved ${details.length} entr${details.length === 1 ? "y" : "ies"}, reclaimed ${humanSize(totalBytes)}.\n`)
|
|
361
|
+
return 0
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function projectsMain(argv: string[]): Promise<number> {
|
|
365
|
+
const options = parseProjectsArgs(argv)
|
|
366
|
+
if (options.error) {
|
|
367
|
+
process.stderr.write(`headlesscode projects: ${options.error}\n\n${PROJECTS_USAGE}`)
|
|
368
|
+
return 2
|
|
369
|
+
}
|
|
370
|
+
if (options.help) {
|
|
371
|
+
process.stdout.write(PROJECTS_USAGE)
|
|
372
|
+
return 0
|
|
373
|
+
}
|
|
374
|
+
if (options.subcommand === "list") {
|
|
375
|
+
return listMain(options)
|
|
376
|
+
}
|
|
377
|
+
if (options.subcommand === "prune") {
|
|
378
|
+
return pruneMain(options)
|
|
379
|
+
}
|
|
380
|
+
process.stderr.write(`headlesscode projects: a subcommand is required\n\n${PROJECTS_USAGE}`)
|
|
381
|
+
return 2
|
|
382
|
+
}
|