@1aboveio/skills 1.0.10 → 1.0.12

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 (214) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +298 -216
  3. package/README.md +3 -3
  4. package/package.json +10 -8
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-foundation/SKILL.md +35 -37
  7. package/skills/app-dev/android-foundation/references/delivery-focus.md +4 -2
  8. package/skills/app-dev/android-foundation/references/feature-work.md +50 -0
  9. package/skills/app-dev/android-foundation/references/setup.md +24 -9
  10. package/skills/app-dev/android-foundation/references/workspace.md +81 -0
  11. package/skills/app-dev/android-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  12. package/skills/app-dev/android-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  13. package/skills/app-dev/android-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  14. package/skills/app-dev/app-debug/SKILL.md +1 -1
  15. package/skills/app-dev/better-auth/SKILL.md +1 -1
  16. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  17. package/skills/app-dev/data-quality/SKILL.md +1 -1
  18. package/skills/app-dev/ios-foundation/SKILL.md +15 -10
  19. package/skills/app-dev/ios-foundation/references/setup.md +25 -9
  20. package/skills/app-dev/ios-foundation/references/workspace.md +82 -0
  21. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/bin/workspaces.mjs +52 -0
  22. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/native-workspace.mjs +239 -0
  23. package/skills/app-dev/ios-foundation/scripts/workspace-runtime/workspace-lifecycle.mjs +2154 -0
  24. package/skills/app-dev/monorepo/SKILL.md +1 -1
  25. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  26. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  27. package/skills/app-dev/pyspark/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/SKILL.md +1 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  31. package/skills/cicd/cloud-build/SKILL.md +1 -1
  32. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  33. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  34. package/skills/cicd/docker/SKILL.md +1 -1
  35. package/skills/cicd/github-actions/SKILL.md +1 -1
  36. package/skills/cicd/github-flow/SKILL.md +1 -1
  37. package/skills/cicd/google-cloud/SKILL.md +1 -1
  38. package/skills/cicd/mergify/SKILL.md +1 -1
  39. package/skills/cicd/podman/SKILL.md +1 -1
  40. package/skills/content/article-review/SKILL.md +1 -1
  41. package/skills/content/article-to-image/SKILL.md +1 -1
  42. package/skills/content/flowchart-svg/SKILL.md +1 -1
  43. package/skills/content/image-gen/SKILL.md +1 -1
  44. package/skills/content/mermaid/SKILL.md +1 -1
  45. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  46. package/skills/content/wechat-publisher/SKILL.md +1 -1
  47. package/skills/content/wechat-search/SKILL.md +1 -1
  48. package/skills/content/xai-search/SKILL.md +1 -1
  49. package/skills/core/distill-writing/SKILL.md +3 -3
  50. package/skills/core/distill-writing/references/extraction.md +1 -1
  51. package/skills/core/doc-to-md/SKILL.md +1 -1
  52. package/skills/core/editorial-review/SKILL.md +82 -12
  53. package/skills/core/editorial-review/references/checklist.md +26 -1
  54. package/skills/core/house-style/SKILL.md +8 -3
  55. package/skills/core/house-style/references/analytical-reporting.md +123 -0
  56. package/skills/core/house-style/references/analytical-writing-patterns.md +79 -0
  57. package/skills/core/house-style/references/analytical-writing-research.md +129 -0
  58. package/skills/core/{write-doc → house-style}/references/bilingual.md +7 -5
  59. package/skills/core/house-style/references/definitions-and-metrics.md +86 -0
  60. package/skills/core/house-style/references/document-workflow.md +193 -0
  61. package/skills/core/house-style/references/initialization.md +66 -0
  62. package/skills/core/house-style/references/persistence.md +9 -4
  63. package/skills/core/house-style/references/rules.md +11 -4
  64. package/skills/core/house-style/scripts/init-guidance.mjs +213 -0
  65. package/skills/core/house-style/templates/analytical-report.md +54 -0
  66. package/skills/core/house-style/templates/gap-analysis.md +1 -1
  67. package/skills/core/plain-language/SKILL.md +2 -2
  68. package/skills/core/polish-cn/SKILL.md +9 -2
  69. package/skills/core/reg-parser/SKILL.md +3 -3
  70. package/skills/core/terminology/SKILL.md +8 -4
  71. package/skills/core/write-doc/SKILL.md +40 -151
  72. package/skills/core/write-report/SKILL.md +129 -0
  73. package/skills/core/{write-doc → write-report}/references/compliance-reports.md +4 -4
  74. package/skills/engineering/better-review/SKILL.md +37 -37
  75. package/skills/engineering/better-review/references/passes.md +35 -41
  76. package/skills/engineering/better-review/references/result.md +10 -9
  77. package/skills/engineering/e2e-test/SKILL.md +17 -17
  78. package/skills/engineering/e2e-test/references/authoring/playwright-config.md +11 -13
  79. package/skills/engineering/e2e-test/references/presentation-sweep.md +7 -7
  80. package/skills/engineering/e2e-test/references/reviewing.md +9 -11
  81. package/skills/engineering/e2e-test/references/route-manifest.md +1 -1
  82. package/skills/engineering/engineering/SKILL.md +201 -0
  83. package/skills/engineering/engineering/references/engineering-conventions.md +32 -0
  84. package/skills/engineering/engineering/references/feature-delivery.md +8 -0
  85. package/skills/engineering/engineering/references/init-scope.md +45 -0
  86. package/skills/engineering/engineering/references/scope-management.md +85 -0
  87. package/skills/engineering/engineering/scripts/init.mjs +160 -0
  88. package/skills/engineering/{contributing → engineering}/scripts/persist-conventions.mjs +1 -1
  89. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  90. package/skills/engineering/engineering-runtime/coherence/workflow.json +19 -19
  91. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -1
  92. package/skills/engineering/ensure-coverage/SKILL.md +22 -109
  93. package/skills/engineering/ensure-coverage/references/breadth/coverage-ledger.md +7 -5
  94. package/skills/engineering/ensure-coverage/references/breadth/surface-baseline.md +4 -4
  95. package/skills/engineering/ensure-coverage/references/breadth/surface-discovery.md +3 -3
  96. package/skills/engineering/ensure-coverage/references/depth/characterization.md +4 -65
  97. package/skills/engineering/ensure-coverage/references/depth/grading.md +5 -7
  98. package/skills/engineering/ensure-coverage/references/depth/mock-policy.md +6 -12
  99. package/skills/engineering/ensure-coverage/references/enforcement/ci-contract.md +1 -1
  100. package/skills/engineering/ensure-coverage/references/presentation/axis.md +6 -6
  101. package/skills/engineering/ensure-coverage/references/process/audit-mode.md +7 -23
  102. package/skills/engineering/ensure-coverage/references/process/output-template.md +18 -133
  103. package/skills/engineering/ensure-coverage/references/process/test-strategy.md +7 -12
  104. package/skills/engineering/ensure-coverage/scripts/README.md +12 -4
  105. package/skills/engineering/golden-path/SKILL.md +1 -1
  106. package/skills/engineering/harness-runtime/SKILL.md +3 -1
  107. package/skills/engineering/harness-runtime/bin/workspaces.mjs +52 -0
  108. package/skills/engineering/harness-runtime/native-workspace.mjs +239 -0
  109. package/skills/engineering/harness-runtime/references/workspaces.md +78 -0
  110. package/skills/engineering/harness-runtime/workspace-lifecycle.mjs +2154 -0
  111. package/skills/engineering/resolve-issues/SKILL.md +40 -61
  112. package/skills/engineering/resolve-issues/references/ci.md +13 -3
  113. package/skills/engineering/resolve-issues/references/delivery-progress.md +4 -3
  114. package/skills/engineering/resolve-issues/references/done.md +11 -2
  115. package/skills/engineering/resolve-issues/references/expire.md +9 -2
  116. package/skills/engineering/resolve-issues/references/implementation.md +33 -17
  117. package/skills/engineering/resolve-issues/references/preflight.md +3 -1
  118. package/skills/engineering/resolve-issues/references/principles.md +52 -0
  119. package/skills/engineering/resolve-issues/references/review.md +25 -15
  120. package/skills/engineering/resolve-issues/references/task-plan.md +64 -27
  121. package/skills/engineering/resolve-issues/references/workspaces.md +164 -0
  122. package/skills/engineering/resolve-issues/scripts/cleanup-runs.mjs +185 -317
  123. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +44 -7
  124. package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +36 -2
  125. package/skills/engineering/resolve-issues/scripts/review-verdict.mjs +9 -8
  126. package/skills/engineering/resolve-issues/scripts/workspace-lifecycle.mjs +1 -0
  127. package/skills/engineering/resolve-release/SKILL.md +1 -1
  128. package/skills/engineering/rush-issues/SKILL.md +7 -5
  129. package/skills/engineering/rush-release/SKILL.md +1 -1
  130. package/skills/engineering/test-quality/SKILL.md +19 -4
  131. package/skills/engineering/test-quality/references/lint.md +1 -1
  132. package/skills/engineering/test-quality/references/principles.md +2 -2
  133. package/skills/engineering/test-quality/references/testing-conventions.md +1 -1
  134. package/skills/risk-compliance/compliance-audit/SKILL.md +4 -8
  135. package/skills/risk-compliance/compliance-audit/references/audience-and-reports.md +12 -0
  136. package/skills/risk-compliance/contract-compare/SKILL.md +17 -1
  137. package/skills/risk-compliance/contract-review/SKILL.md +6 -1
  138. package/skills/risk-compliance/contract-review/references/report-stage.md +17 -0
  139. package/skills/risk-compliance/doc-authenticity/SKILL.md +11 -1
  140. package/skills/risk-compliance/fraud-detection/LICENSE +3 -0
  141. package/skills/risk-compliance/fraud-detection/SKILL.md +148 -0
  142. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/first-party-fraud.md +3 -2
  143. package/skills/risk-compliance/fraud-detection/references/archetypes/third-party-fraud.md +47 -0
  144. package/skills/risk-compliance/fraud-detection/references/collection-payout-features.md +151 -0
  145. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/contexts/bank-transfer.md +4 -3
  146. package/skills/risk-compliance/fraud-detection/references/contexts/card-payment.md +70 -0
  147. package/skills/risk-compliance/fraud-detection/references/contexts/payment-collection.md +34 -0
  148. package/skills/risk-compliance/fraud-detection/references/contexts/payout.md +34 -0
  149. package/skills/risk-compliance/fraud-detection/references/enrichment.md +74 -0
  150. package/skills/risk-compliance/fraud-detection/references/entities.md +48 -0
  151. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/feature-engineering.md +69 -33
  152. package/skills/risk-compliance/fraud-detection/references/feature-sql.md +150 -0
  153. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-rationale.md +4 -2
  154. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/report-template.md +45 -23
  155. package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/review-checklist.md +60 -20
  156. package/skills/risk-compliance/fraud-detection/references/workflow.md +216 -0
  157. package/skills/risk-compliance/fraud-detection/release-check.mjs +95 -0
  158. package/skills/risk-compliance/payment-analysis/SKILL.md +36 -17
  159. package/skills/risk-compliance/payment-analysis/references/auth-rate-actions.md +2 -2
  160. package/skills/risk-compliance/payment-analysis/references/report-template.md +10 -2
  161. package/skills/risk-compliance/payment-analysis/references/visualization.md +11 -1
  162. package/skills/risk-compliance/portfolio-payment-analysis/LICENSE +3 -0
  163. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +170 -0
  164. package/skills/risk-compliance/portfolio-payment-analysis/references/identity-and-reconciliation.md +46 -0
  165. package/skills/risk-compliance/portfolio-payment-analysis/references/metrics-and-reporting.md +170 -0
  166. package/skills/risk-compliance/portfolio-payment-analysis/references/risk-history.md +89 -0
  167. package/skills/risk-compliance/portfolio-payment-analysis/release-check.mjs +95 -0
  168. package/skills/risk-compliance/portfolio-payment-analysis/scripts/analyze.py +636 -0
  169. package/skills/risk-compliance/portfolio-payment-analysis/scripts/build_report.py +386 -0
  170. package/skills/risk-compliance/portfolio-payment-analysis/scripts/comparison_charts.py +425 -0
  171. package/skills/risk-compliance/portfolio-payment-analysis/scripts/csv_export.py +52 -0
  172. package/skills/risk-compliance/portfolio-payment-analysis/scripts/render.py +160 -0
  173. package/skills/risk-compliance/portfolio-payment-analysis/scripts/risk_history.py +435 -0
  174. package/skills/risk-compliance/similar-domain/SKILL.md +9 -4
  175. package/skills/risk-compliance/tld-detection/SKILL.md +124 -239
  176. package/skills/risk-compliance/tld-detection/agents/openai.yaml +1 -1
  177. package/skills/risk-compliance/tld-detection/references/clean.md +18 -51
  178. package/skills/risk-compliance/tld-detection/references/fx-conversion.md +56 -219
  179. package/skills/risk-compliance/tld-detection/references/metric-contract.md +109 -0
  180. package/skills/risk-compliance/tld-detection/references/pattern.md +99 -0
  181. package/skills/risk-compliance/tld-detection/references/report-template.md +49 -272
  182. package/skills/risk-compliance/tld-detection/scripts/detect_ladders.py +46 -13
  183. package/skills/risk-compliance/tld-detection/scripts/detect_patterns.py +346 -0
  184. package/skills/risk-compliance/tld-detection/scripts/detect_streams.py +51 -19
  185. package/skills/risk-compliance/tld-detection/scripts/profile_groups.py +109 -0
  186. package/skills/risk-compliance/tld-detection/scripts/render_charts.py +218 -0
  187. package/skills/risk-compliance/tld-detection/scripts/tld_common.py +243 -0
  188. package/skills/engineering/contributing/SKILL.md +0 -70
  189. package/skills/risk-compliance/fraud-analysis/SKILL.md +0 -114
  190. package/skills/risk-compliance/fraud-analysis/references/archetypes/third-party-fraud.md +0 -27
  191. package/skills/risk-compliance/fraud-analysis/references/contexts/card-payment.md +0 -30
  192. package/skills/risk-compliance/fraud-analysis/references/contexts/payment-collection.md +0 -20
  193. package/skills/risk-compliance/fraud-analysis/references/contexts/payout.md +0 -20
  194. package/skills/risk-compliance/fraud-analysis/references/workflow.md +0 -175
  195. package/skills/risk-compliance/tld-detection/references/chinese-polish.md +0 -55
  196. package/skills/risk-compliance/tld-detection/references/clustering.md +0 -235
  197. package/skills/risk-compliance/tld-detection/references/decision.md +0 -189
  198. package/skills/risk-compliance/tld-detection/references/streams.md +0 -141
  199. package/skills/risk-compliance/tld-detection/references/terminology.md +0 -95
  200. package/skills/risk-compliance/tld-detection/references/workflow.md +0 -95
  201. /package/skills/{engineering/contributing → core/write-report}/LICENSE +0 -0
  202. /package/skills/core/{write-doc → write-report}/references/reports/audit-report-template.md +0 -0
  203. /package/skills/core/{write-doc → write-report}/references/reports/external-assessment-template.md +0 -0
  204. /package/skills/core/{write-doc → write-report}/references/reports/management-report-template.md +0 -0
  205. /package/skills/core/{write-doc → write-report}/references/reports/vars-report-template.md +0 -0
  206. /package/skills/{engineering/contributing → core/write-report}/release-check.mjs +0 -0
  207. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/LICENSE +0 -0
  208. /package/skills/engineering/{contributing → engineering}/references/tracker-conventions.md +0 -0
  209. /package/skills/{risk-compliance/fraud-analysis → engineering/engineering}/release-check.mjs +0 -0
  210. /package/skills/engineering/{contributing → engineering}/scripts/persist-conventions-core.mjs +0 -0
  211. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/archetypes/authorized-payment-scam.md +0 -0
  212. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/mechanisms/account-takeover.md +0 -0
  213. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/taxonomy.md +0 -0
  214. /package/skills/risk-compliance/{fraud-analysis → fraud-detection}/references/terminology.md +0 -0
@@ -1,364 +1,232 @@
1
1
  #!/usr/bin/env node
2
- // Reclaim disk from finished resolve-issues runs without losing work or evidence.
2
+ // Owned unit workspaces: prepare, clean up, recover, and inventory.
3
3
  //
4
- // cleanup-runs.mjs [--repo <dir>] [--older-than-days <n>] [--log-days <n>] [--apply]
5
- // cleanup-runs.mjs --merged <unit-workspace> [--apply]
4
+ // cleanup-runs.mjs prepare --run <run dir> --unit <id> --revision <n> [--apply] [--json] [--kind worktree|clone]
5
+ // cleanup-runs.mjs cleanup --run <run dir> --unit <id> --revision <n> [--apply] [--json]
6
+ // cleanup-runs.mjs recover --repo <repo root> --json
7
+ // cleanup-runs.mjs inventory --repo <repo root> [--root <dir>] [--max-entries <n>] [--max-depth <n>] --json
6
8
  //
7
- // Sweep mode looks at every run under <repo>/.resolve-issues/rush/, aged by its
8
- // task-plan.json (the file the orchestrator saves on every transition):
9
+ // Legacy entry points, kept because the coordinator's references still call them:
9
10
  //
10
- // idle under --older-than-days (default 3) untouched
11
- // idle under --log-days (default 30) downloads go; records and logs stay
12
- // idle longer records stay; logs go too
11
+ // cleanup-runs.mjs --repo <repo root> [--apply] [--json]
12
+ // cleanup-runs.mjs --merged <unit-workspace-or-resource> [--apply] [--json]
13
13
  //
14
- // Downloads are everything that can be fetched or rebuilt: tool caches, SDKs,
15
- // browsers, emulator disks, installs, builds, and a clean clone's tracked source
16
- // and .git. Logs are text evidence (.log, .txt, .json, .jsonl, .md, .out, .err,
17
- // logcat) up to EVIDENCE_MAX_BYTES, wherever they sit, except inside a tool
18
- // cache; inside a non-Git copy of a repository (a folder with a lockfile) only
19
- // .log and logcat files, and text under test-results/, logs/, and similar. Records are the run's own small .json, .jsonl and .md files near the
20
- // top of the run, outside any clone.
14
+ // --repo is a safe owned completed-unit sweep, never an age-based disk sweep. It only
15
+ // considers units the plan records as succeeded with delivery merged, and then applies the
16
+ // same safety checks as `cleanup`. --merged resolves the saved run and unit that own the
17
+ // path; an unowned path is refused. Neither mode strips ignored files, deletes logs, or
18
+ // removes anything by mtime.
21
19
  //
22
- // A Git workspace never loses work. A clean one with nothing unpushed loses its
23
- // tracked files, .git and downloads and keeps its logs. One with uncommitted
24
- // changes, unpushed commits, or a stash keeps everything git tracks or would
25
- // show as untracked; only its git-ignored paths are swept.
26
- //
27
- // --merged handles one unit workspace whose PR has merged: its commits are on
28
- // the target, so it counts as clean unless it has uncommitted changes or a
29
- // stash. Its logs stay.
30
- //
31
- // Without --apply nothing is touched; the actions are printed. The last line is
32
- // always a one-line summary. Exit 0 on success, 2 for a bad command.
20
+ // The helper never writes task-plan.json. `prepare` and `cleanup` return the next lifecycle
21
+ // as JSON; the coordinator persists it through update_plan. A successful removal writes a
22
+ // postcondition receipt under <run>/results/<unit>-cleanup.json.
33
23
 
34
24
  import { execFileSync } from 'node:child_process'
35
- import { existsSync, lstatSync, readdirSync, rmdirSync, rmSync, statSync, statfsSync } from 'node:fs'
36
- import { dirname, join, relative, resolve, sep } from 'node:path'
25
+ import { existsSync } from 'node:fs'
37
26
 
38
27
  import { isMainModule } from '../../engineering-runtime/scripts/main-module.mjs'
28
+ import {
29
+ INVENTORY_MAX_DEPTH, INVENTORY_MAX_ENTRIES, LifecycleError, cleanup, cleanupMerged, inventory, prepare, recover, sweep,
30
+ } from './workspace-lifecycle.mjs'
39
31
 
40
- export const DEFAULT_OLDER_THAN_DAYS = 3
41
- export const DEFAULT_LOG_DAYS = 30
42
- // Records live near the top of a run: task-plan.json, exploration/, sources/,
43
- // reviews/<round>/review.json.
44
- export const RECORD_MAX_DEPTH = 4
45
- export const RECORD_MAX_BYTES = 1024 * 1024
46
- export const EVIDENCE_MAX_BYTES = 20 * 1024 * 1024
47
- const RECORD_EXTENSIONS = ['.json', '.jsonl', '.md']
48
- const EVIDENCE_EXTENSIONS = ['.log', '.txt', '.json', '.jsonl', '.md', '.out', '.err']
49
- // Directories that only ever hold downloads or build output.
50
- export const CACHE_DIRS = new Set([
51
- 'node_modules', '.cache', 'cache', 'caches', 'xdg-cache', '.gradle', '.npm', '.pnpm-store', '.m2', '.android',
52
- 'android-sdk', 'sdk', 'system-images', 'build', 'dist', '.cxx', '.next', '.turbo', 'browsers',
53
- 'playwright-browsers', 'ms-playwright', '.venv', 'venv', '__pycache__', 'DerivedData', 'Pods',
54
- ])
55
- // A non-Git folder with one of these at its root is a copy of a repository:
56
- // inside it, only logs and text under an evidence folder count as evidence.
57
- const LOCKFILES = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock', 'bun.lockb', 'uv.lock', 'poetry.lock', 'Cargo.lock', 'go.sum', 'Gemfile.lock']
58
- const EVIDENCE_DIRS = new Set(['test-results', 'logs', 'log', 'reports', 'artifacts', 'playwright-report'])
59
- const DAY_MS = 24 * 60 * 60 * 1000
32
+ export { LifecycleError } from './workspace-lifecycle.mjs'
60
33
 
61
- export class CleanupError extends Error {}
34
+ const USAGE = `usage: cleanup-runs.mjs prepare --run <run dir> --unit <id> --revision <n> [--apply] [--json] [--kind worktree|clone]
35
+ cleanup-runs.mjs cleanup --run <run dir> --unit <id> --revision <n> [--apply] [--json]
36
+ cleanup-runs.mjs recover --repo <repo root> --json
37
+ cleanup-runs.mjs inventory --repo <repo root> [--root <dir>] [--max-entries <n>] [--max-depth <n>] --json
38
+ cleanup-runs.mjs --repo <repo root> [--apply] [--json]
39
+ cleanup-runs.mjs --merged <unit-workspace-or-resource> [--apply] [--json]
62
40
 
63
- function git(dir, args) {
64
- return execFileSync('git', ['-C', dir, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 256 * 1024 * 1024 })
65
- }
41
+ exit 0: the command ran and every removal it planned succeeded, or it was a read-only plan
42
+ exit 1: a path was retained/refused for a safety reason, or --apply could not finish a removal
43
+ exit 2: bad arguments, unreadable or contradictory plan, revision mismatch, lock held, or unowned path`
66
44
 
67
- // What unsaved work a Git workspace holds. `null` when Git cannot read it.
68
- export function workspaceState(dir) {
69
- try {
70
- const changed = git(dir, ['status', '--porcelain']).split('\n').filter(Boolean).length
71
- const unpushed = Number(git(dir, ['rev-list', '--count', 'HEAD', '--branches', '--not', '--remotes']).trim())
72
- const stashed = git(dir, ['stash', 'list']).split('\n').filter(Boolean).length
73
- return { changed, unpushed, stashed }
74
- } catch {
75
- return null
76
- }
77
- }
45
+ const COMMANDS = new Set(['prepare', 'cleanup', 'recover', 'inventory'])
78
46
 
79
- const isGitWorkspace = (dir) => existsSync(join(dir, '.git'))
80
- const isCacheDir = (name) => CACHE_DIRS.has(name) || name.endsWith('.avd')
81
- const isEvidenceName = (name) => EVIDENCE_EXTENSIONS.some((ext) => name.endsWith(ext)) || /logcat/i.test(name)
82
- const isLogName = (name) => name.endsWith('.log') || /logcat/i.test(name)
83
- const isSourceCopy = (dir) => LOCKFILES.some((f) => existsSync(join(dir, f)))
47
+ class UsageError extends Error {}
84
48
 
85
- // Entering a repository copy starts fresh: only evidence folders inside it count.
86
- function enterDir(ctx, name, path) {
87
- if (isSourceCopy(path)) return { sourceCopy: true, inEvidenceDir: false }
88
- return { sourceCopy: ctx.sourceCopy, inEvidenceDir: ctx.inEvidenceDir || EVIDENCE_DIRS.has(name) }
49
+ function positiveInt(value, flag, { allowZero = false } = {}) {
50
+ const n = Number(value)
51
+ if (!Number.isSafeInteger(n) || n < (allowZero ? 0 : 1)) throw new UsageError(`${flag} must be ${allowZero ? 'a nonnegative' : 'a positive'} integer`)
52
+ return n
89
53
  }
90
54
 
91
- function describe(state) {
92
- const parts = []
93
- if (state.changed) parts.push(`${state.changed} uncommitted file(s)`)
94
- if (state.unpushed) parts.push(`${state.unpushed} unpushed commit(s)`)
95
- if (state.stashed) parts.push(`${state.stashed} stash(es)`)
96
- return parts.join(', ')
97
- }
98
-
99
- // What happens to one file: kept as a record, kept as a log, or deleted.
100
- function fileFate(name, path, ctx) {
101
- if (ctx.tracked?.has(path)) return 'delete'
102
- const size = statSync(path).size
103
- const isRecord = !ctx.inClone && !ctx.hidden && ctx.depth <= RECORD_MAX_DEPTH
104
- && RECORD_EXTENSIONS.some((ext) => name.endsWith(ext)) && size <= RECORD_MAX_BYTES
105
- if (isRecord) return 'record'
106
- const evidence = ctx.sourceCopy && !ctx.inEvidenceDir ? isLogName(name) : isEvidenceName(name)
107
- if (ctx.keepLogs && evidence && size <= EVIDENCE_MAX_BYTES) return 'log'
108
- return 'delete'
109
- }
110
-
111
- // Actions for one directory. Returns { actions, kept, logs }; a directory with
112
- // nothing kept collapses into a single delete.
113
- function sweep(dir, ctx) {
114
- const actions = []
115
- let kept = false
116
- let logs = 0
117
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
118
- const path = join(dir, entry.name)
119
- if (entry.isDirectory()) {
120
- if (isCacheDir(entry.name) || (ctx.inClone && entry.name === '.git')) {
121
- actions.push({ kind: 'delete', path })
122
- continue
123
- }
124
- if (!ctx.inClone && isGitWorkspace(path)) {
125
- const action = workspaceAction(path, { keepLogs: ctx.keepLogs, depth: ctx.depth + 1 })
126
- actions.push(action)
127
- logs += action.logs ?? 0
128
- if (action.kind !== 'delete') kept = true
129
- continue
130
- }
131
- const sub = sweep(path, {
132
- ...ctx,
133
- depth: ctx.depth + 1,
134
- hidden: ctx.hidden || entry.name.startsWith('.'),
135
- ...enterDir(ctx, entry.name, path),
136
- })
137
- if (sub.kept) {
138
- actions.push(...sub.actions)
139
- kept = true
140
- logs += sub.logs
141
- } else {
142
- actions.push({ kind: 'delete', path })
143
- }
144
- } else if (entry.isFile()) {
145
- const fate = fileFate(entry.name, path, ctx)
146
- if (fate === 'delete') actions.push({ kind: 'delete', path })
147
- else {
148
- kept = true
149
- if (fate === 'log') logs++
150
- }
151
- } else {
152
- actions.push({ kind: 'delete', path })
153
- }
55
+ export function parseArgs(argv) {
56
+ const opts = {
57
+ command: null, run: null, unit: null, revision: null, repo: null, merged: null,
58
+ apply: false, json: false, help: false, kind: null, root: null,
59
+ maxEntries: INVENTORY_MAX_ENTRIES, maxDepth: INVENTORY_MAX_DEPTH,
154
60
  }
155
- return { actions, kept, logs }
156
- }
157
-
158
- const zList = (out) => out.split('\0').filter(Boolean)
159
-
160
- export function workspaceAction(dir, { merged = false, keepLogs = true, depth = 1 } = {}) {
161
- const state = workspaceState(dir)
162
- if (!state) return { kind: 'keep', path: dir, reason: 'Git cannot read this workspace', deletes: [], logs: 0 }
163
- const unsaved = { ...state, unpushed: merged ? 0 : state.unpushed }
164
- const ctx = { depth, hidden: false, inClone: true, keepLogs }
165
- if (!unsaved.changed && !unsaved.unpushed && !unsaved.stashed) {
166
- const tracked = new Set(zList(git(dir, ['ls-files', '-z'])).map((p) => join(dir, p)))
167
- const sub = sweep(dir, { ...ctx, tracked })
168
- const reason = merged ? 'merged and clean' : 'clean and pushed'
169
- if (!sub.kept) return { kind: 'delete', path: dir, reason, deletes: [], logs: 0 }
170
- return { kind: 'sweep', path: dir, reason: `${reason}; keeps ${sub.logs} log file(s)`, deletes: sub.actions, logs: sub.logs }
171
- }
172
- // Only git-ignored paths are fair game; tracked and untracked files are work.
173
- const deletes = []
174
- let logs = 0
175
- for (const rel of zList(git(dir, ['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z']))) {
176
- const path = join(dir, rel)
177
- if (!existsSync(path)) continue
178
- const name = rel.replace(/\/$/, '').split('/').pop()
179
- if (lstatSync(path).isDirectory()) {
180
- if (isCacheDir(name)) { deletes.push({ kind: 'delete', path }); continue }
181
- const sub = sweep(path, { ...ctx, depth: depth + rel.split('/').length, ...enterDir({ ...ctx, sourceCopy: false, inEvidenceDir: false }, name, path) })
182
- if (sub.kept) { deletes.push(...sub.actions); logs += sub.logs } else deletes.push({ kind: 'delete', path })
183
- } else if (fileFate(name, path, ctx) === 'delete') {
184
- deletes.push({ kind: 'delete', path })
185
- } else {
186
- logs++
61
+ for (let i = 0; i < argv.length; i++) {
62
+ const arg = argv[i]
63
+ if (COMMANDS.has(arg) && !opts.command) { opts.command = arg; continue }
64
+ if (arg === '--apply') { opts.apply = true; continue }
65
+ if (arg === '--json') { opts.json = true; continue }
66
+ if (arg === '--help' || arg === '-h') { opts.help = true; continue }
67
+ const value = () => {
68
+ if (argv[i + 1] === undefined || argv[i + 1].startsWith('--')) throw new UsageError(`${arg} needs a value`)
69
+ return argv[++i]
187
70
  }
188
- }
189
- return { kind: 'strip', path: dir, reason: `keeps ${describe(unsaved)} and ${logs} log file(s); removes ignored downloads`, deletes, logs }
190
- }
191
-
192
- function lastActive(runDir) {
193
- const plan = join(runDir, 'task-plan.json')
194
- return (existsSync(plan) ? statSync(plan) : statSync(runDir)).mtimeMs
71
+ if (arg === '--run') opts.run = value()
72
+ else if (arg === '--unit') opts.unit = value()
73
+ else if (arg === '--revision') opts.revision = positiveInt(value(), '--revision', { allowZero: true })
74
+ else if (arg === '--repo') opts.repo = value()
75
+ else if (arg === '--merged') opts.merged = value()
76
+ else if (arg === '--kind') opts.kind = value()
77
+ else if (arg === '--root') opts.root = value()
78
+ else if (arg === '--max-entries') opts.maxEntries = positiveInt(value(), '--max-entries')
79
+ else if (arg === '--max-depth') opts.maxDepth = positiveInt(value(), '--max-depth', { allowZero: true })
80
+ else if (arg === '--older-than-days' || arg === '--log-days') {
81
+ throw new UsageError(`${arg} was removed: age no longer authorizes a deletion. A workspace is cleaned only from task-plan ownership, completion, and the safety checks. Run inventory to see what is on disk`)
82
+ } else throw new UsageError(`unrecognized argument ${arg}`)
83
+ }
84
+ if (!opts.command && !opts.merged && !opts.repo) throw new UsageError('no command, --repo, or --merged given')
85
+ if (opts.command === 'prepare' || opts.command === 'cleanup') {
86
+ if (!opts.run) throw new UsageError(`${opts.command} needs --run`)
87
+ if (!opts.unit) throw new UsageError(`${opts.command} needs --unit`)
88
+ if (opts.revision === null) throw new UsageError(`${opts.command} needs --revision`)
89
+ if (opts.merged) throw new UsageError(`${opts.command} does not take --merged`)
90
+ }
91
+ if ((opts.command === 'recover' || opts.command === 'inventory') && !opts.repo) throw new UsageError(`${opts.command} needs --repo`)
92
+ if (opts.command === 'recover' && opts.apply) throw new UsageError('recover is read-only and does not take --apply')
93
+ if (opts.command === 'inventory' && opts.apply) throw new UsageError('inventory is read-only and does not take --apply')
94
+ if (opts.command === 'prepare' && opts.root) throw new UsageError('prepare does not take --root')
95
+ if (opts.command && opts.merged) throw new UsageError(`${opts.command} does not take --merged`)
96
+ return opts
195
97
  }
196
98
 
197
- export function planSweep({ repo, olderThanDays = DEFAULT_OLDER_THAN_DAYS, logDays = DEFAULT_LOG_DAYS, now = Date.now() }) {
198
- const root = join(repo, '.resolve-issues', 'rush')
199
- if (!existsSync(root)) return { root, runs: [] }
200
- const runs = []
201
- for (const entry of readdirSync(root, { withFileTypes: true })) {
202
- if (!entry.isDirectory()) continue
203
- const dir = join(root, entry.name)
204
- const idleDays = (now - lastActive(dir)) / DAY_MS
205
- if (idleDays < olderThanDays) {
206
- runs.push({ run: entry.name, dir, idleDays, stage: 'active', actions: [], logs: 0 })
207
- continue
208
- }
209
- const keepLogs = idleDays < logDays
210
- const result = sweep(dir, { depth: 1, hidden: false, inClone: false, keepLogs })
211
- runs.push({ run: entry.name, dir, idleDays, stage: keepLogs ? 'keep-logs' : 'records-only', actions: result.actions, logs: result.logs })
212
- }
213
- runs.sort((a, b) => a.run.localeCompare(b.run))
214
- return { root, runs }
99
+ function ghAvailableRunner() {
100
+ if (process.env.CLEANUP_RUNS_NO_GH === '1') return null
101
+ return (args) => execFileSync('gh', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000, maxBuffer: 4 * 1024 * 1024 })
215
102
  }
216
103
 
217
- export function planMerged(workspace) {
218
- const dir = resolve(workspace)
219
- if (!dir.split(sep).includes('.resolve-issues')) {
220
- throw new CleanupError(`--merged only removes workspaces under a .resolve-issues directory: ${dir}`)
221
- }
222
- if (!existsSync(dir) || !lstatSync(dir).isDirectory() || !isGitWorkspace(dir)) {
223
- throw new CleanupError(`not a Git workspace: ${dir}`)
224
- }
225
- return workspaceAction(dir, { merged: true, keepLogs: true })
104
+ function printJson(value) {
105
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
226
106
  }
227
107
 
228
- // Remove directories left empty under `dir` (bottom-up), and `dir` itself.
229
- function pruneEmpty(dir) {
230
- if (!existsSync(dir) || !lstatSync(dir).isDirectory()) return
231
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
232
- if (entry.isDirectory()) pruneEmpty(join(dir, entry.name))
108
+ function failure(command, error) {
109
+ const code = error instanceof LifecycleError || error instanceof UsageError ? error.code ?? 'usage' : 'error'
110
+ return {
111
+ version: 1,
112
+ command,
113
+ ok: false,
114
+ applied: false,
115
+ action: 'refuse',
116
+ reason: error.message,
117
+ code,
118
+ error: { code, message: error.message, ...(error.details ? { details: error.details } : {}) },
119
+ errors: [error.message],
233
120
  }
234
- if (readdirSync(dir).length === 0) rmdirSync(dir)
235
121
  }
236
122
 
237
- // Delete one path. Package stores (pnpm, Gradle) mark their files and folders
238
- // read-only, so on a permission failure grant the owner write access and retry.
239
- // Returns an error message, or null on success.
240
- function remove(path) {
241
- try {
242
- rmSync(path, { recursive: true, force: true })
243
- return null
244
- } catch {
245
- try {
246
- execFileSync('chmod', ['u+w', dirname(path)], { stdio: 'ignore' })
247
- execFileSync('chmod', ['-R', 'u+w', path], { stdio: 'ignore' })
248
- rmSync(path, { recursive: true, force: true })
249
- return null
250
- } catch (error) {
251
- return `${path}: ${error.code ?? error.message}`
252
- }
253
- }
123
+ function renderPrepare(result) {
124
+ const lines = []
125
+ const dry = result.applied ? '' : ' (dry run)'
126
+ lines.push(`prepare${dry}: ${result.action} ${result.workspace.kind} ${result.workspace.path}`)
127
+ lines.push(` base ${result.workspace.baseSha} revision ${result.revision} unit ${result.unit}`)
128
+ if (result.resources.length) lines.push(` resources: ${result.resources.map((r) => `${r.role}${r.path}`).join(', ')}`)
129
+ if (!result.applied) lines.push(' nothing was created; rerun with --apply')
130
+ return lines.join('\n')
254
131
  }
255
132
 
256
- // Apply one action; returns the paths it could not remove.
257
- export function applyAction(action) {
258
- const failures = []
259
- const del = (path) => {
260
- const failure = remove(path)
261
- if (failure) failures.push(failure)
262
- }
263
- if (action.kind === 'delete') del(action.path)
264
- else if (action.kind === 'sweep' || action.kind === 'strip') {
265
- for (const d of action.deletes) del(d.path)
266
- if (action.kind === 'sweep' && failures.length === 0) pruneEmpty(action.path)
267
- }
268
- return failures
133
+ function renderCleanup(result) {
134
+ const lines = []
135
+ const verb = result.applied ? '' : ' (dry run)'
136
+ const target = result.workspace?.path ?? result.paths?.[0] ?? ''
137
+ lines.push(`${result.command}${verb}: ${result.action} ${target}`)
138
+ if (result.reason) lines.push(` ${result.reason}`)
139
+ for (const check of result.checks ?? []) lines.push(` ${check.ok ? 'ok ' : 'FAIL'} ${check.name}: ${check.detail}`)
140
+ if (result.receipt) lines.push(` receipt ${result.receipt}`)
141
+ if (result.wouldRemove) lines.push(' nothing was removed; rerun with --apply')
142
+ return lines.join('\n')
269
143
  }
270
144
 
271
- const deletionCount = (actions) => actions.reduce((n, a) => n + (a.kind === 'delete' ? 1 : (a.deletes?.length ?? 0)), 0)
272
-
273
- function freeBytes(dir) {
274
- try {
275
- const s = statfsSync(dir)
276
- return s.bavail * s.bsize
277
- } catch {
278
- return null
145
+ function renderSweep(result) {
146
+ const lines = [`sweep${result.applied ? '' : ' (dry run)'}: ${result.repo}`]
147
+ for (const run of result.runs) {
148
+ lines.push(` run ${run.run}${run.revision !== null ? ` (revision ${run.revision})` : ''}`)
149
+ for (const unit of run.units) {
150
+ lines.push(` ${unit.unit}: ${unit.action}${unit.reason ? ` — ${unit.reason}` : ''}`)
151
+ }
279
152
  }
153
+ if (result.unknownEntries.length) lines.push(` unknown entries (inventory only, never removed): ${result.unknownEntries.join(', ')}`)
154
+ const s = result.summary
155
+ lines.push(` ${s.runs} run(s), ${s.completed} completed unit(s): ${s.removed} removed, ${s.retained} retained, ${s.refused} refused; ${s.unknown} unknown entr${s.unknown === 1 ? 'y' : 'ies'}`)
156
+ return lines.join('\n')
280
157
  }
281
158
 
282
- const gigabytes = (bytes) => `${(bytes / 1e9).toFixed(1)} GB`
283
-
284
- function parseArgs(argv) {
285
- const usage = 'usage: cleanup-runs.mjs [--repo <dir>] [--older-than-days <n>] [--log-days <n>] [--apply] | --merged <unit-workspace> [--apply]'
286
- const opts = { repo: process.cwd(), olderThanDays: DEFAULT_OLDER_THAN_DAYS, logDays: DEFAULT_LOG_DAYS, apply: false, merged: null }
287
- const days = (value) => {
288
- const n = Number(value)
289
- if (!Number.isFinite(n) || n < 0) throw new CleanupError(usage)
290
- return n
291
- }
292
- for (let i = 0; i < argv.length; i++) {
293
- const arg = argv[i]
294
- if (arg === '--apply') opts.apply = true
295
- else if (arg === '--repo' && argv[i + 1]) opts.repo = argv[++i]
296
- else if (arg === '--merged' && argv[i + 1]) opts.merged = argv[++i]
297
- else if (arg === '--older-than-days' && argv[i + 1]) opts.olderThanDays = days(argv[++i])
298
- else if (arg === '--log-days' && argv[i + 1]) opts.logDays = days(argv[++i])
299
- else throw new CleanupError(usage)
159
+ function renderRecover(result) {
160
+ const lines = [`recover (read-only): ${result.repo}`]
161
+ for (const run of result.runs) {
162
+ for (const unit of run.units) lines.push(` ${unit.unit}: ${unit.action}${unit.reason ? ` — ${unit.reason}` : ''}`)
300
163
  }
301
- if (opts.logDays < opts.olderThanDays) throw new CleanupError('--log-days must be at least --older-than-days')
302
- return opts
164
+ lines.push(` ${result.due.length} due operation(s), ${result.worktrees.length} registered worktree(s), ${result.locks.length} lock(s)`)
165
+ return lines.join('\n')
303
166
  }
304
167
 
305
- function describeAction(a, base) {
306
- const extra = a.deletes?.length ? `; ${a.deletes.length} path(s) to delete` : ''
307
- return `${a.kind.padEnd(6)} ${relative(base, a.path) || '.'}${a.reason ? ` (${a.reason}${extra})` : ''}`
168
+ function renderInventory(result) {
169
+ const lines = [`inventory (read-only): ${result.root}`]
170
+ lines.push(` ${result.entries.length} entr${result.entries.length === 1 ? 'y' : 'ies'}${result.truncated ? ' (truncated)' : ''}; ${result.runs.length} run(s); ${result.registeredWorktrees.length} registered worktree(s)`)
171
+ if (result.reason) lines.push(` ${result.reason}`)
172
+ return lines.join('\n')
308
173
  }
309
174
 
310
- function run(opts, out) {
311
- const base = opts.merged ? resolve(opts.merged, '..') : opts.repo
312
- const before = opts.apply ? freeBytes(base) : null
313
- let actions
314
- let summary
315
- if (opts.merged) {
316
- const action = planMerged(opts.merged)
317
- out(describeAction(action, base))
318
- actions = [action]
319
- summary = `${action.kind} ${action.path}`
320
- } else {
321
- const plan = planSweep({ repo: resolve(opts.repo), olderThanDays: opts.olderThanDays, logDays: opts.logDays })
322
- actions = []
323
- let logs = 0
324
- for (const r of plan.runs) {
325
- const idle = `idle ${r.idleDays.toFixed(1)} days`
326
- if (r.stage === 'active') {
327
- out(`run ${r.run} (${idle}): skipped, used within ${opts.olderThanDays} days`)
328
- continue
329
- }
330
- const note = r.stage === 'records-only' ? `records only, past ${opts.logDays} days` : `keeps records and ${r.logs} log file(s)`
331
- out(`run ${r.run} (${idle}): ${note}${r.actions.length ? '' : '; nothing to remove'}`)
332
- for (const a of r.actions) out(` ${describeAction(a, r.dir)}`)
333
- actions.push(...r.actions)
334
- logs += r.logs
175
+ export function main(argv) {
176
+ let opts
177
+ let command = argv.find((arg) => COMMANDS.has(arg)) ?? (argv.includes('--merged') ? 'merged' : 'sweep')
178
+ try {
179
+ opts = parseArgs(argv)
180
+ if (opts.help) { process.stdout.write(`${USAGE}\n`); return 0 }
181
+ command = opts.command ?? (opts.merged ? 'merged' : 'sweep')
182
+ const runGh = ghAvailableRunner()
183
+
184
+ if (command === 'prepare') {
185
+ const result = prepare({ run: opts.run, unit: opts.unit, revision: opts.revision, apply: opts.apply, kind: opts.kind })
186
+ if (opts.json) printJson(result)
187
+ else process.stdout.write(`${renderPrepare(result)}\n`)
188
+ return 0
335
189
  }
336
- const swept = plan.runs.filter((r) => r.stage !== 'active').length
337
- const count = (kind) => actions.filter((a) => a.kind === kind).length
338
- summary = `${swept} idle run(s), ${plan.runs.length - swept} active; ${deletionCount(actions)} path(s) deleted, ${count('sweep')} clean clone(s) emptied, ${count('strip')} clone(s) with work stripped, ${count('keep')} unreadable kept; ${logs} log file(s) kept`
339
- }
340
- if (!opts.apply) {
341
- out(`cleanup-runs: dry run, nothing removed; ${summary}. Rerun with --apply.`)
342
- return
343
- }
344
- const failures = actions.flatMap((action) => applyAction(action))
345
- for (const failure of failures) out(` could not remove ${failure}`)
346
- if (!opts.merged) {
347
- try {
348
- git(resolve(opts.repo), ['worktree', 'prune'])
349
- } catch {}
190
+ if (command === 'cleanup') {
191
+ const result = cleanup({ run: opts.run, unit: opts.unit, revision: opts.revision, apply: opts.apply, requireApplying: true, runGh })
192
+ if (opts.json) printJson(result)
193
+ else process.stdout.write(`${renderCleanup(result)}\n`)
194
+ return result.ok ? 0 : (result.action === 'refuse' ? 2 : 1)
195
+ }
196
+ if (command === 'recover') {
197
+ const result = recover({ repo: opts.repo, runGh })
198
+ if (opts.json) printJson(result)
199
+ else process.stdout.write(`${renderRecover(result)}\n`)
200
+ return result.ok ? 0 : 2
201
+ }
202
+ if (command === 'inventory') {
203
+ const result = inventory({ repo: opts.repo, root: opts.root, maxEntries: opts.maxEntries, maxDepth: opts.maxDepth })
204
+ if (opts.json) printJson(result)
205
+ else process.stdout.write(`${renderInventory(result)}\n`)
206
+ return 0
207
+ }
208
+ if (command === 'merged') {
209
+ const result = cleanupMerged({ repo: opts.repo, path: opts.merged, apply: opts.apply, requireApplying: false, runGh: ghAvailableRunner() })
210
+ if (opts.json) printJson(result)
211
+ else process.stdout.write(`${renderCleanup(result)}\n`)
212
+ return result.ok ? 0 : 1
213
+ }
214
+ if (command === 'sweep') {
215
+ const result = sweep({ repo: opts.repo, apply: opts.apply, runGh })
216
+ if (opts.json) printJson(result)
217
+ else process.stdout.write(`${renderSweep(result)}\n`)
218
+ return result.ok ? 0 : 2
219
+ }
220
+ throw new UsageError(USAGE)
221
+ } catch (error) {
222
+ if (opts?.json) printJson(failure(command, error))
223
+ else process.stderr.write(`${error instanceof UsageError ? `${error.message}\n${USAGE}` : `${error.message}\n`}`)
224
+ if (error instanceof UsageError) return 2
225
+ if (error instanceof LifecycleError) return 2
226
+ return 2
350
227
  }
351
- const after = freeBytes(base)
352
- const freed = before !== null && after !== null ? `; freed ${gigabytes(Math.max(0, after - before))}` : ''
353
- const failed = failures.length ? `; ${failures.length} path(s) could not be removed` : ''
354
- out(`cleanup-runs: ${summary}${freed}${failed}`)
355
228
  }
356
229
 
357
230
  if (isMainModule(import.meta.url)) {
358
- try {
359
- run(parseArgs(process.argv.slice(2)), (line) => process.stdout.write(`${line}\n`))
360
- } catch (error) {
361
- process.stderr.write(`${error instanceof CleanupError ? error.message : error.stack}\n`)
362
- process.exitCode = 2
363
- }
231
+ process.exitCode = main(process.argv.slice(2))
364
232
  }