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.
Files changed (232) hide show
  1. package/ATTRIBUTION.md +53 -0
  2. package/CODE_OF_CONDUCT.md +130 -0
  3. package/CONTRIBUTING.md +107 -0
  4. package/LICENSE +202 -0
  5. package/README.md +486 -0
  6. package/SECURITY.md +211 -0
  7. package/bin/headlesscode.mjs +83 -0
  8. package/package.json +63 -0
  9. package/shared/prompts/review-mode-prompt-short.md +93 -0
  10. package/shared/prompts/review-mode-prompt.md +281 -0
  11. package/shared/rules-code/rules.md +22 -0
  12. package/shared/stacks/cpp/rules.md +30 -0
  13. package/shared/stacks/fastapi/rules.md +30 -0
  14. package/shared/stacks/javascript/rules.md +37 -0
  15. package/shared/stacks/postgresql/rules.md +31 -0
  16. package/shared/stacks/python/rules.md +35 -0
  17. package/shared/stacks/react/rules.md +11 -0
  18. package/shared/stacks/typescript/rules.md +10 -0
  19. package/src/budget/budget.ts +221 -0
  20. package/src/budget/concurrency.ts +126 -0
  21. package/src/budget/cost.ts +309 -0
  22. package/src/budget/index.ts +8 -0
  23. package/src/checkpoints/cli.ts +256 -0
  24. package/src/checkpoints/service.ts +227 -0
  25. package/src/cli.ts +1535 -0
  26. package/src/cloud/docker-provider.ts +334 -0
  27. package/src/cloud/provider.ts +300 -0
  28. package/src/codeintel/call-graph.ts +78 -0
  29. package/src/codeintel/find-references.ts +123 -0
  30. package/src/codeintel/go-to-definition.ts +193 -0
  31. package/src/codeintel/handlers.ts +190 -0
  32. package/src/codeintel/import-graph.ts +173 -0
  33. package/src/codeintel/outline.ts +180 -0
  34. package/src/codeintel/position.ts +77 -0
  35. package/src/codeintel/program.ts +350 -0
  36. package/src/codeintel/rename-symbol.ts +213 -0
  37. package/src/codeintel/tools.ts +280 -0
  38. package/src/codemap/build.ts +135 -0
  39. package/src/codemap/cli.ts +190 -0
  40. package/src/codemap/extract.ts +339 -0
  41. package/src/codemap/files.ts +236 -0
  42. package/src/codemap/fingerprint.ts +65 -0
  43. package/src/codemap/flows.ts +62 -0
  44. package/src/codemap/html.ts +451 -0
  45. package/src/codemap/lock.ts +80 -0
  46. package/src/codemap/types.ts +101 -0
  47. package/src/codesearch/airunner-embedder.ts +185 -0
  48. package/src/codesearch/chunk.ts +339 -0
  49. package/src/codesearch/cli.ts +223 -0
  50. package/src/codesearch/embedder.ts +332 -0
  51. package/src/codesearch/files.ts +280 -0
  52. package/src/codesearch/index.ts +469 -0
  53. package/src/codesearch/ollama-embedder.ts +205 -0
  54. package/src/codesearch/search.ts +141 -0
  55. package/src/codesearch/types.ts +100 -0
  56. package/src/config/mode-models.ts +218 -0
  57. package/src/dashboard/aggregate.ts +364 -0
  58. package/src/dashboard/chat-thread.ts +141 -0
  59. package/src/dashboard/checkpoints.ts +124 -0
  60. package/src/dashboard/cli.ts +193 -0
  61. package/src/dashboard/codemap.ts +44 -0
  62. package/src/dashboard/files.ts +121 -0
  63. package/src/dashboard/page.ts +2803 -0
  64. package/src/dashboard/self-improvement-metrics.ts +282 -0
  65. package/src/dashboard/server.ts +1103 -0
  66. package/src/dashboard/session-launch.ts +310 -0
  67. package/src/dashboard/timeline.ts +273 -0
  68. package/src/dashboard/tool-exec.ts +107 -0
  69. package/src/dashboard/trend-cli.ts +141 -0
  70. package/src/dashboard/trend.ts +413 -0
  71. package/src/decision-proxy/cli.ts +261 -0
  72. package/src/decision-proxy/proxy.ts +569 -0
  73. package/src/deploy/gate-cli.ts +147 -0
  74. package/src/deploy/gate.ts +254 -0
  75. package/src/engine/condense.ts +512 -0
  76. package/src/engine/events.ts +428 -0
  77. package/src/engine/handoff.ts +71 -0
  78. package/src/engine/lazy-tools.ts +160 -0
  79. package/src/engine/local-explore.ts +653 -0
  80. package/src/engine/logger.ts +96 -0
  81. package/src/engine/loop.ts +5517 -0
  82. package/src/engine/parser.ts +347 -0
  83. package/src/engine/prompt.ts +860 -0
  84. package/src/engine/reports.ts +47 -0
  85. package/src/engine/stacks.ts +448 -0
  86. package/src/engine/types.ts +291 -0
  87. package/src/engine/usage.ts +186 -0
  88. package/src/github/app-auth.ts +161 -0
  89. package/src/github/cli.ts +448 -0
  90. package/src/github/installations.ts +133 -0
  91. package/src/github/pr.ts +321 -0
  92. package/src/github/provision.ts +118 -0
  93. package/src/github/push.ts +122 -0
  94. package/src/index-util.ts +50 -0
  95. package/src/index.ts +81 -0
  96. package/src/init/cli.ts +248 -0
  97. package/src/init/gitignore.ts +74 -0
  98. package/src/llm/ollama.ts +308 -0
  99. package/src/llm/openrouter.ts +868 -0
  100. package/src/llm/preflight.ts +367 -0
  101. package/src/llm/transcript-capture.ts +84 -0
  102. package/src/memory/embed.ts +110 -0
  103. package/src/memory/index.ts +22 -0
  104. package/src/memory/local.ts +259 -0
  105. package/src/memory/summarizer.ts +283 -0
  106. package/src/memory/types.ts +153 -0
  107. package/src/memory/uwuchat.ts +157 -0
  108. package/src/migrate/cli.ts +115 -0
  109. package/src/orchestrator/analyze-cli.ts +104 -0
  110. package/src/orchestrator/auto-split.ts +206 -0
  111. package/src/orchestrator/cleanup.ts +1003 -0
  112. package/src/orchestrator/cli.ts +3571 -0
  113. package/src/orchestrator/cost-estimate.ts +564 -0
  114. package/src/orchestrator/cost-history-cli.ts +242 -0
  115. package/src/orchestrator/cost-history.ts +397 -0
  116. package/src/orchestrator/git-sync.ts +250 -0
  117. package/src/orchestrator/index.ts +153 -0
  118. package/src/orchestrator/log-analysis.ts +0 -0
  119. package/src/orchestrator/merge-check.ts +108 -0
  120. package/src/orchestrator/pipeline.ts +411 -0
  121. package/src/orchestrator/resume.ts +1940 -0
  122. package/src/orchestrator/reviewer.ts +503 -0
  123. package/src/orchestrator/split.ts +296 -0
  124. package/src/orchestrator/state.ts +542 -0
  125. package/src/orchestrator/status.ts +697 -0
  126. package/src/orchestrator/verification-gate.ts +134 -0
  127. package/src/orchestrator/watch.ts +898 -0
  128. package/src/permissions/commands.ts +1083 -0
  129. package/src/permissions/config.ts +241 -0
  130. package/src/permissions/index.ts +12 -0
  131. package/src/permissions/protected-files.ts +96 -0
  132. package/src/permissions/store-protection.ts +272 -0
  133. package/src/project-store.ts +648 -0
  134. package/src/projects/cli.ts +382 -0
  135. package/src/qa/qa.ts +487 -0
  136. package/src/tools/browser/handler.ts +346 -0
  137. package/src/tools/browser/service.ts +406 -0
  138. package/src/tools/browser/smoke.ts +78 -0
  139. package/src/tools/browser/tool.ts +99 -0
  140. package/src/tools/executor.ts +2575 -0
  141. package/src/tools/language-detect.ts +183 -0
  142. package/src/tools/output-summarizer.ts +369 -0
  143. package/src/tools/run-tests.ts +302 -0
  144. package/src/tools/set-indentation-tool.ts +49 -0
  145. package/src/tools/test-selection.ts +160 -0
  146. package/src/vendor/tests/smoke.ts +103 -0
  147. package/src/vendor/zoo-code/VENDOR-NOTES.md +213 -0
  148. package/src/vendor/zoo-code/shim/anthropic.ts +71 -0
  149. package/src/vendor/zoo-code/shim/openai.d.ts +60 -0
  150. package/src/vendor/zoo-code/shim/os-name.ts +18 -0
  151. package/src/vendor/zoo-code/shim/strip-bom.ts +14 -0
  152. package/src/vendor/zoo-code/shim/vscode.ts +76 -0
  153. package/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +1015 -0
  154. package/src/vendor/zoo-code/src/core/diff/strategies/multi-search-replace.ts +670 -0
  155. package/src/vendor/zoo-code/src/core/prompts/sections/capabilities.ts +46 -0
  156. package/src/vendor/zoo-code/src/core/prompts/sections/custom-instructions.ts +559 -0
  157. package/src/vendor/zoo-code/src/core/prompts/sections/index.ts +10 -0
  158. package/src/vendor/zoo-code/src/core/prompts/sections/markdown-formatting.ts +7 -0
  159. package/src/vendor/zoo-code/src/core/prompts/sections/modes.ts +35 -0
  160. package/src/vendor/zoo-code/src/core/prompts/sections/objective.ts +13 -0
  161. package/src/vendor/zoo-code/src/core/prompts/sections/rules.ts +95 -0
  162. package/src/vendor/zoo-code/src/core/prompts/sections/skills.ts +105 -0
  163. package/src/vendor/zoo-code/src/core/prompts/sections/system-info.ts +30 -0
  164. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use-guidelines.ts +9 -0
  165. package/src/vendor/zoo-code/src/core/prompts/sections/tool-use.ts +7 -0
  166. package/src/vendor/zoo-code/src/core/prompts/system.ts +176 -0
  167. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/access_mcp_resource.ts +41 -0
  168. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_diff.ts +40 -0
  169. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/apply_patch.ts +61 -0
  170. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/ask_followup_question.ts +62 -0
  171. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/attempt_completion.ts +33 -0
  172. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/codebase_search.ts +43 -0
  173. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/converters.ts +109 -0
  174. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit.ts +48 -0
  175. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/edit_file.ts +72 -0
  176. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/execute_command.ts +54 -0
  177. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/generate_image.ts +51 -0
  178. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/index.ts +75 -0
  179. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/list_files.ts +41 -0
  180. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/mcp_server.ts +75 -0
  181. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/new_task.ts +39 -0
  182. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_command_output.ts +81 -0
  183. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/read_file.ts +169 -0
  184. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/run_slash_command.ts +31 -0
  185. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_files.ts +50 -0
  186. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/search_replace.ts +51 -0
  187. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/skill.ts +33 -0
  188. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/switch_mode.ts +31 -0
  189. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/update_todo_list.ts +54 -0
  190. package/src/vendor/zoo-code/src/core/prompts/tools/native-tools/write_to_file.ts +40 -0
  191. package/src/vendor/zoo-code/src/core/prompts/types.ts +12 -0
  192. package/src/vendor/zoo-code/src/i18n/index.ts +19 -0
  193. package/src/vendor/zoo-code/src/integrations/misc/extract-text.ts +81 -0
  194. package/src/vendor/zoo-code/src/services/checkpoints/RepoPerTaskCheckpointService.ts +15 -0
  195. package/src/vendor/zoo-code/src/services/checkpoints/ShadowCheckpointService.ts +553 -0
  196. package/src/vendor/zoo-code/src/services/checkpoints/excludes.ts +212 -0
  197. package/src/vendor/zoo-code/src/services/checkpoints/index.ts +3 -0
  198. package/src/vendor/zoo-code/src/services/checkpoints/types.ts +35 -0
  199. package/src/vendor/zoo-code/src/services/code-index/manager.ts +19 -0
  200. package/src/vendor/zoo-code/src/services/mcp/McpHub.ts +36 -0
  201. package/src/vendor/zoo-code/src/services/roo-config/index.ts +441 -0
  202. package/src/vendor/zoo-code/src/services/search/file-search.ts +143 -0
  203. package/src/vendor/zoo-code/src/services/skills/SkillsManager.ts +20 -0
  204. package/src/vendor/zoo-code/src/shared/globalFileNames.ts +9 -0
  205. package/src/vendor/zoo-code/src/shared/language.ts +43 -0
  206. package/src/vendor/zoo-code/src/shared/modes.ts +257 -0
  207. package/src/vendor/zoo-code/src/shared/tools.ts +385 -0
  208. package/src/vendor/zoo-code/src/utils/fs.ts +39 -0
  209. package/src/vendor/zoo-code/src/utils/globalContext.ts +22 -0
  210. package/src/vendor/zoo-code/src/utils/json-schema.ts +16 -0
  211. package/src/vendor/zoo-code/src/utils/logging.ts +21 -0
  212. package/src/vendor/zoo-code/src/utils/mcp-name.ts +190 -0
  213. package/src/vendor/zoo-code/src/utils/object.ts +18 -0
  214. package/src/vendor/zoo-code/src/utils/path.ts +94 -0
  215. package/src/vendor/zoo-code/src/utils/shell.ts +376 -0
  216. package/src/vendor/zoo-code/src/utils/text-normalization.ts +99 -0
  217. package/src/vendor/zoo-code/types/global-settings.ts +19 -0
  218. package/src/vendor/zoo-code/types/index.ts +22 -0
  219. package/src/vendor/zoo-code/types/message.ts +375 -0
  220. package/src/vendor/zoo-code/types/mode.ts +241 -0
  221. package/src/vendor/zoo-code/types/todo.ts +19 -0
  222. package/src/vendor/zoo-code/types/tool-params.ts +116 -0
  223. package/src/vendor/zoo-code/types/tool.ts +67 -0
  224. package/src/vendor/zoo-code/types/vscode.ts +84 -0
  225. package/src/vision/describe.ts +242 -0
  226. package/src/vision/tool.ts +91 -0
  227. package/src/watcher/cli.ts +369 -0
  228. package/src/watcher/github.ts +304 -0
  229. package/src/watcher/index.ts +59 -0
  230. package/src/watcher/state.ts +254 -0
  231. package/src/watcher/watch.ts +562 -0
  232. 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
+ }