@workerstack/repo 0.0.0-stage → 0.1.0

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 (438) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +0 -3
  4. package/agents/global-settings/antigravity.json +3 -0
  5. package/agents/global-settings/claude.json +6 -0
  6. package/agents/global-settings/codex.toml +2 -0
  7. package/agents/global-settings/cursor.json +3 -0
  8. package/agents/global-settings/grok.toml +36 -0
  9. package/agents/global-settings/manifest.json +36 -0
  10. package/agents/grok/hooks/orchestrate-parent.json +60 -0
  11. package/agents/grok/hooks/rtk.json +16 -0
  12. package/agents/grok/hooks/rulesync.json +3 -0
  13. package/agents/grok/roles/scout.toml +5 -0
  14. package/agents/hooks.jsonc +67 -0
  15. package/agents/mcp-policy.jsonc +16 -0
  16. package/agents/rules/overview.md +21 -0
  17. package/agents/settings/claude.json +5 -0
  18. package/agents/skills/ask/SKILL.md +13 -0
  19. package/agents/skills/auto/SKILL.md +13 -0
  20. package/agents/skills/away/SKILL.md +19 -0
  21. package/agents/skills/blast-radius/SKILL.md +54 -0
  22. package/agents/skills/bro/SKILL.md +9 -0
  23. package/agents/skills/browser/SKILL.md +16 -0
  24. package/agents/skills/clasp/SKILL.md +11 -0
  25. package/agents/skills/commit/SKILL.md +35 -0
  26. package/agents/skills/commit-and-push/SKILL.md +9 -0
  27. package/agents/skills/commit-push/SKILL.md +59 -0
  28. package/agents/skills/commit-push-deploy/SKILL.md +74 -0
  29. package/agents/skills/commit-push-release/SKILL.md +75 -0
  30. package/agents/skills/correct/SKILL.md +39 -0
  31. package/agents/skills/do/SKILL.md +21 -0
  32. package/agents/skills/explain/SKILL.md +13 -0
  33. package/agents/skills/google-workspace/SKILL.md +12 -0
  34. package/agents/skills/handoff/SKILL.md +81 -0
  35. package/agents/skills/implement-plan/SKILL.md +29 -0
  36. package/agents/skills/implement-roadmap/SKILL.md +27 -0
  37. package/agents/skills/jev/SKILL.md +17 -0
  38. package/agents/skills/listen/SKILL.md +15 -0
  39. package/agents/skills/pickup/SKILL.md +58 -0
  40. package/agents/skills/principle-attack-the-premise/SKILL.md +24 -0
  41. package/agents/skills/principle-boundary-discipline/SKILL.md +38 -0
  42. package/agents/skills/principle-build-the-lever/SKILL.md +28 -0
  43. package/agents/skills/principle-encode-lessons-in-structure/SKILL.md +15 -0
  44. package/agents/skills/principle-exhaust-the-design-space/SKILL.md +12 -0
  45. package/agents/skills/principle-experience-first/SKILL.md +22 -0
  46. package/agents/skills/principle-explain-the-number/SKILL.md +21 -0
  47. package/agents/skills/principle-fix-root-causes/SKILL.md +24 -0
  48. package/agents/skills/principle-foundational-thinking/SKILL.md +25 -0
  49. package/agents/skills/principle-guard-the-context-window/SKILL.md +16 -0
  50. package/agents/skills/principle-laziness-protocol/SKILL.md +13 -0
  51. package/agents/skills/principle-make-operations-idempotent/SKILL.md +25 -0
  52. package/agents/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +18 -0
  53. package/agents/skills/principle-minimize-reader-load/SKILL.md +22 -0
  54. package/agents/skills/principle-model-the-domain/SKILL.md +26 -0
  55. package/agents/skills/principle-never-block-on-the-human/SKILL.md +17 -0
  56. package/agents/skills/principle-outcome-oriented-execution/SKILL.md +25 -0
  57. package/agents/skills/principle-prove-it-works/SKILL.md +25 -0
  58. package/agents/skills/principle-redesign-from-first-principles/SKILL.md +18 -0
  59. package/agents/skills/principle-separate-before-serializing-shared-state/SKILL.md +20 -0
  60. package/agents/skills/principle-sequence-verifiable-units/SKILL.md +22 -0
  61. package/agents/skills/principle-subtract-before-you-add/SKILL.md +21 -0
  62. package/agents/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
  63. package/agents/skills/principle-type-system-discipline/SKILL.md +17 -0
  64. package/agents/skills/review-plan/SKILL.md +28 -0
  65. package/agents/skills/review-roadmap/SKILL.md +28 -0
  66. package/agents/skills/show-me/SKILL.md +21 -0
  67. package/agents/skills/speak/SKILL.md +15 -0
  68. package/agents/skills/su/SKILL.md +11 -0
  69. package/agents/skills/teach/SKILL.md +25 -0
  70. package/agents/skills/tmp/SKILL.md +54 -0
  71. package/agents/skills/typescript-best-practices/SKILL.md +32 -0
  72. package/agents/skills/typescript-best-practices/references/patterns.md +328 -0
  73. package/agents/skills/unslop/SKILL.md +62 -0
  74. package/agents/skills/write-plan/SKILL.md +33 -0
  75. package/agents/skills/write-roadmap/SKILL.md +66 -0
  76. package/agents/subagents/implementer-deep.md +57 -0
  77. package/agents/subagents/implementer-fast.md +57 -0
  78. package/agents/subagents/orchestrator.md +43 -0
  79. package/agents/subagents/researcher.md +47 -0
  80. package/agents/subagents/scout.md +44 -0
  81. package/bin/repo.ts +4 -0
  82. package/cli/actionlint-default.test.ts +38 -0
  83. package/cli/actionlint-download.test.ts +162 -0
  84. package/cli/actionlint-real.test.ts +33 -0
  85. package/cli/actionlint.constants.ts +43 -0
  86. package/cli/actionlint.test.ts +108 -0
  87. package/cli/actionlint.ts +178 -0
  88. package/cli/bin.test.ts +54 -0
  89. package/cli/bin.ts +40 -0
  90. package/cli/check-changelog.test.ts +40 -0
  91. package/cli/check-changelog.ts +12 -0
  92. package/cli/check-comments.test.ts +157 -0
  93. package/cli/check-comments.ts +123 -0
  94. package/cli/check-env-modules.test.ts +85 -0
  95. package/cli/check-env-modules.ts +56 -0
  96. package/cli/check-magic.test.ts +139 -0
  97. package/cli/check-magic.ts +120 -0
  98. package/cli/check-manifests.constants.ts +4 -0
  99. package/cli/check-manifests.test.ts +210 -0
  100. package/cli/check-manifests.ts +71 -0
  101. package/cli/check-nightly.test.ts +103 -0
  102. package/cli/check-nightly.ts +37 -0
  103. package/cli/check-ratchets-missing.test.ts +46 -0
  104. package/cli/check-ratchets-mutation.test.ts +49 -0
  105. package/cli/check-ratchets.constants.ts +20 -0
  106. package/cli/check-ratchets.test.ts +190 -0
  107. package/cli/check-ratchets.ts +193 -0
  108. package/cli/check-rejections.test.ts +220 -0
  109. package/cli/check-rejections.ts +153 -0
  110. package/cli/check-reports.ts +37 -0
  111. package/cli/check-tsconfig.test.ts +507 -0
  112. package/cli/check-tsconfig.ts +205 -0
  113. package/cli/commands.test.ts +114 -0
  114. package/cli/commands.ts +218 -0
  115. package/cli/commit-changelog.ts +27 -0
  116. package/cli/commit-msg-changelog.test.ts +93 -0
  117. package/cli/commit-msg-range.test.ts +126 -0
  118. package/cli/commit-msg-red-proof.test.ts +147 -0
  119. package/cli/commit-msg-repo.test.ts +153 -0
  120. package/cli/commit-msg.test.ts +101 -0
  121. package/cli/commit-msg.ts +194 -0
  122. package/cli/commit-range.constants.ts +17 -0
  123. package/cli/commit-range.ts +57 -0
  124. package/cli/config-knip.test.ts +70 -0
  125. package/cli/config-nightly.test.ts +84 -0
  126. package/cli/config-tdd.test.ts +46 -0
  127. package/cli/config.test.ts +576 -0
  128. package/cli/config.ts +261 -0
  129. package/cli/conventional.ts +9 -0
  130. package/cli/coverage-config.ts +62 -0
  131. package/cli/coverage.constants.ts +6 -0
  132. package/cli/coverage.test.ts +199 -0
  133. package/cli/coverage.ts +127 -0
  134. package/cli/env.constants.ts +10 -0
  135. package/cli/env.test.ts +56 -0
  136. package/cli/env.ts +72 -0
  137. package/cli/fixtures/base-config.ts +13 -0
  138. package/cli/fixtures/gate-world.ts +48 -0
  139. package/cli/fixtures/knip-run.ts +38 -0
  140. package/cli/fixtures/mutate-fakes.ts +137 -0
  141. package/cli/fixtures/planted-repo.ts +50 -0
  142. package/cli/fixtures/publish-world.ts +71 -0
  143. package/cli/fixtures/red-proof-repo.ts +72 -0
  144. package/cli/fixtures/release-context.ts +15 -0
  145. package/cli/fixtures/release-world.ts +111 -0
  146. package/cli/fixtures/stub-bin.ts +40 -0
  147. package/cli/guard-diagnostics.test.ts +157 -0
  148. package/cli/guard-diagnostics.ts +195 -0
  149. package/cli/init.test.ts +487 -0
  150. package/cli/init.ts +275 -0
  151. package/cli/knip-check-ignores.test.ts +106 -0
  152. package/cli/knip-check-real.test.ts +65 -0
  153. package/cli/knip-check.constants.ts +46 -0
  154. package/cli/knip-check.test.ts +114 -0
  155. package/cli/knip-check.ts +169 -0
  156. package/cli/knip-schema.ts +67 -0
  157. package/cli/known-failures.constants.ts +22 -0
  158. package/cli/known-failures.test.ts +138 -0
  159. package/cli/known-failures.ts +131 -0
  160. package/cli/lint/as-needs-reason.ts +41 -0
  161. package/cli/lint/boundaries.ts +73 -0
  162. package/cli/lint/class-file.ts +39 -0
  163. package/cli/lint/config-file.ts +32 -0
  164. package/cli/lint/entry-file.ts +54 -0
  165. package/cli/lint/env-in-config.ts +66 -0
  166. package/cli/lint/filename-matches-export.ts +63 -0
  167. package/cli/lint/index-file.ts +22 -0
  168. package/cli/lint/index.ts +47 -0
  169. package/cli/lint/is-reason.ts +6 -0
  170. package/cli/lint/manifest.ts +46 -0
  171. package/cli/lint/names.ts +59 -0
  172. package/cli/lint/no-commented-out-code.ts +105 -0
  173. package/cli/lint/no-mock-of-subject.ts +64 -0
  174. package/cli/lint/no-module-side-effects.ts +94 -0
  175. package/cli/lint/no-module-state.ts +58 -0
  176. package/cli/lint/no-side-effect-import.ts +17 -0
  177. package/cli/lint/no-v8-ignore.ts +18 -0
  178. package/cli/lint/script-rules.ts +9 -0
  179. package/cli/lint/skip-on-platform.constants.ts +8 -0
  180. package/cli/lint/skip-on-platform.ts +118 -0
  181. package/cli/lint/todo-needs-id.ts +63 -0
  182. package/cli/lint/types-file.ts +28 -0
  183. package/cli/lint-schema.ts +32 -0
  184. package/cli/magic/fixtures/magic-repo.ts +64 -0
  185. package/cli/magic/magic-hit.ts +2 -0
  186. package/cli/magic/magic-nodes.ts +44 -0
  187. package/cli/magic/magic-numbers.test.ts +99 -0
  188. package/cli/magic/magic-numbers.ts +117 -0
  189. package/cli/magic/magic-strings.test.ts +38 -0
  190. package/cli/magic/magic-strings.ts +81 -0
  191. package/cli/main.test.ts +853 -0
  192. package/cli/main.ts +292 -0
  193. package/cli/mutate-args.ts +79 -0
  194. package/cli/mutate-check.test.ts +147 -0
  195. package/cli/mutate-deps.ts +42 -0
  196. package/cli/mutate-edges.test.ts +150 -0
  197. package/cli/mutate-files.test.ts +100 -0
  198. package/cli/mutate-files.ts +31 -0
  199. package/cli/mutate-floors.test.ts +91 -0
  200. package/cli/mutate-floors.ts +104 -0
  201. package/cli/mutate-real-deps.ts +57 -0
  202. package/cli/mutate-real.test.ts +96 -0
  203. package/cli/mutate-report.ts +96 -0
  204. package/cli/mutate-run.ts +128 -0
  205. package/cli/mutate-shards.ts +64 -0
  206. package/cli/mutate.constants.ts +67 -0
  207. package/cli/mutate.test.ts +182 -0
  208. package/cli/mutate.ts +64 -0
  209. package/cli/nested-vitest-flags.ts +8 -0
  210. package/cli/nightly-config.constants.ts +14 -0
  211. package/cli/nightly-config.ts +62 -0
  212. package/cli/prompt.test.ts +202 -0
  213. package/cli/prompt.ts +1 -0
  214. package/cli/red-proof-edges.test.ts +78 -0
  215. package/cli/red-proof-failures.test.ts +82 -0
  216. package/cli/red-proof-tree.test.ts +157 -0
  217. package/cli/red-proof-verdict.test.ts +44 -0
  218. package/cli/red-proof.constants.ts +14 -0
  219. package/cli/red-proof.ts +202 -0
  220. package/cli/release/changelog-parse.ts +83 -0
  221. package/cli/release/changelog.test.ts +141 -0
  222. package/cli/release/changelog.ts +106 -0
  223. package/cli/release/gate-refusals.test.ts +85 -0
  224. package/cli/release/gate.test.ts +63 -0
  225. package/cli/release/gate.ts +70 -0
  226. package/cli/release/git.ts +66 -0
  227. package/cli/release/github.test.ts +128 -0
  228. package/cli/release/github.ts +46 -0
  229. package/cli/release/list-dir.test.ts +11 -0
  230. package/cli/release/list-dir.ts +6 -0
  231. package/cli/release/main.test.ts +48 -0
  232. package/cli/release/main.ts +51 -0
  233. package/cli/release/manifest.test.ts +24 -0
  234. package/cli/release/manifest.ts +34 -0
  235. package/cli/release/pack.test.ts +107 -0
  236. package/cli/release/pack.ts +76 -0
  237. package/cli/release/plan.ts +127 -0
  238. package/cli/release/publish-run.test.ts +98 -0
  239. package/cli/release/publish.test.ts +127 -0
  240. package/cli/release/publish.ts +125 -0
  241. package/cli/release/refuse.ts +5 -0
  242. package/cli/release/release-messages.constants.ts +96 -0
  243. package/cli/release/release-refusals.test.ts +149 -0
  244. package/cli/release/release.constants.ts +128 -0
  245. package/cli/release/release.test.ts +199 -0
  246. package/cli/release/release.ts +145 -0
  247. package/cli/release/version.test.ts +118 -0
  248. package/cli/release/version.ts +69 -0
  249. package/cli/release/wait.test.ts +101 -0
  250. package/cli/release/wait.ts +95 -0
  251. package/cli/script-diagnostics.test.ts +76 -0
  252. package/cli/script-diagnostics.ts +62 -0
  253. package/cli/scripts/board.ts +519 -0
  254. package/cli/scripts/browser.ts +994 -0
  255. package/cli/scripts/change-set.ts +105 -0
  256. package/cli/scripts/check-globals.ts +199 -0
  257. package/cli/scripts/ci-skips.constants.ts +17 -0
  258. package/cli/scripts/ci-skips.ts +118 -0
  259. package/cli/scripts/ci-wait-run.ts +28 -0
  260. package/cli/scripts/ci-wait.constants.ts +44 -0
  261. package/cli/scripts/ci-wait.ts +175 -0
  262. package/cli/scripts/ci.constants.ts +15 -0
  263. package/cli/scripts/ci.ts +208 -0
  264. package/cli/scripts/clean.ts +363 -0
  265. package/cli/scripts/comments-step.ts +15 -0
  266. package/cli/scripts/doctor.ts +828 -0
  267. package/cli/scripts/emit-agent-shell-tools.ts +91 -0
  268. package/cli/scripts/emit-mcp-policy.ts +402 -0
  269. package/cli/scripts/folder-map.ts +353 -0
  270. package/cli/scripts/gate-fat-tools.ts +1095 -0
  271. package/cli/scripts/gitignore-guard.ts +338 -0
  272. package/cli/scripts/google-workspace.ts +779 -0
  273. package/cli/scripts/hook-command.ts +72 -0
  274. package/cli/scripts/hook-input.ts +178 -0
  275. package/cli/scripts/hook-io.constants.ts +5 -0
  276. package/cli/scripts/hook-io.ts +46 -0
  277. package/cli/scripts/hooks-union.ts +469 -0
  278. package/cli/scripts/interactive.ts +30 -0
  279. package/cli/scripts/jev.ts +420 -0
  280. package/cli/scripts/job.ts +934 -0
  281. package/cli/scripts/jsonc.ts +9 -0
  282. package/cli/scripts/lessons.ts +564 -0
  283. package/cli/scripts/lint-baseline.ts +233 -0
  284. package/cli/scripts/lint-main.ts +24 -0
  285. package/cli/scripts/lint-skips.ts +139 -0
  286. package/cli/scripts/lint.constants.ts +6 -0
  287. package/cli/scripts/lint.ts +142 -0
  288. package/cli/scripts/listen.ts +962 -0
  289. package/cli/scripts/managed-text.ts +47 -0
  290. package/cli/scripts/orchestrate-parent.ts +1155 -0
  291. package/cli/scripts/parse-need.ts +81 -0
  292. package/cli/scripts/persist-handoff.ts +228 -0
  293. package/cli/scripts/persist-implementation.ts +276 -0
  294. package/cli/scripts/persist-plan.ts +888 -0
  295. package/cli/scripts/persist-roadmap.ts +478 -0
  296. package/cli/scripts/persist-tmp.ts +177 -0
  297. package/cli/scripts/persist.ts +54 -0
  298. package/cli/scripts/phase-files.ts +121 -0
  299. package/cli/scripts/prompt.ts +38 -0
  300. package/cli/scripts/purge-global-mcps.ts +313 -0
  301. package/cli/scripts/rejections-step.ts +37 -0
  302. package/cli/scripts/rtk-grok.ts +102 -0
  303. package/cli/scripts/scrub-spawn-prompt.ts +103 -0
  304. package/cli/scripts/scrub-text-lib.ts +58 -0
  305. package/cli/scripts/session-map.ts +69 -0
  306. package/cli/scripts/setup.ts +862 -0
  307. package/cli/scripts/skill-conflicts.ts +82 -0
  308. package/cli/scripts/speak.ts +370 -0
  309. package/cli/scripts/strategy.ts +168 -0
  310. package/cli/scripts/sync-global-settings.ts +316 -0
  311. package/cli/scripts/turn-steps.ts +249 -0
  312. package/cli/scripts/turn.ts +157 -0
  313. package/cli/scripts/typecheck-step.ts +43 -0
  314. package/cli/scripts/vscode-tasks.ts +175 -0
  315. package/cli/scripts/workspace-root.ts +160 -0
  316. package/cli/scripts/worktree.ts +806 -0
  317. package/cli/spawn.test.ts +34 -0
  318. package/cli/spawn.ts +27 -0
  319. package/cli/staged-change.constants.ts +11 -0
  320. package/cli/staged-change.ts +79 -0
  321. package/cli/stryker-preload.test.ts +50 -0
  322. package/cli/stryker-preload.ts +9 -0
  323. package/cli/sync/agents/frontmatter.test.ts +197 -0
  324. package/cli/sync/agents/frontmatter.ts +140 -0
  325. package/cli/sync/agents/hooks.test.ts +517 -0
  326. package/cli/sync/agents/hooks.ts +401 -0
  327. package/cli/sync/agents/rules.test.ts +229 -0
  328. package/cli/sync/agents/rules.ts +84 -0
  329. package/cli/sync/agents/sources.test.ts +375 -0
  330. package/cli/sync/agents/sources.ts +244 -0
  331. package/cli/sync/agents/subagents.test.ts +149 -0
  332. package/cli/sync/agents/subagents.ts +158 -0
  333. package/cli/sync/agents-text.test.ts +31 -0
  334. package/cli/sync/agents-text.ts +36 -0
  335. package/cli/sync/fixtures/copied.README.md +35 -0
  336. package/cli/sync/fixtures/copied.tgz +0 -0
  337. package/cli/sync/fixtures/maps/golden/INDEX.md +22 -0
  338. package/cli/sync/fixtures/maps-ws.tgz +0 -0
  339. package/cli/sync/git-files.test.ts +340 -0
  340. package/cli/sync/git-files.ts +197 -0
  341. package/cli/sync/git-hooks.test.ts +94 -0
  342. package/cli/sync/golden.test.ts +15 -0
  343. package/cli/sync/hooks-mode.test.ts +99 -0
  344. package/cli/sync/hosts.ts +28 -0
  345. package/cli/sync/knip-config.constants.ts +4 -0
  346. package/cli/sync/knip-config.test.ts +147 -0
  347. package/cli/sync/knip-config.ts +108 -0
  348. package/cli/sync/manifest.test.ts +78 -0
  349. package/cli/sync/manifest.ts +60 -0
  350. package/cli/sync/maps.test.ts +304 -0
  351. package/cli/sync/maps.ts +297 -0
  352. package/cli/sync/migrate.test.ts +2000 -0
  353. package/cli/sync/migrate.ts +1104 -0
  354. package/cli/sync/nightly-jobs.test.ts +63 -0
  355. package/cli/sync/nightly.constants.ts +48 -0
  356. package/cli/sync/nightly.test.ts +100 -0
  357. package/cli/sync/nightly.ts +75 -0
  358. package/cli/sync/outputs.test.ts +351 -0
  359. package/cli/sync/outputs.ts +168 -0
  360. package/cli/sync/oxlint-config.test.ts +178 -0
  361. package/cli/sync/oxlint-config.ts +168 -0
  362. package/cli/sync/release-jobs.test.ts +29 -0
  363. package/cli/sync/release-reports.test.ts +31 -0
  364. package/cli/sync/release-workflow.constants.ts +61 -0
  365. package/cli/sync/release-workflow.test.ts +192 -0
  366. package/cli/sync/release-workflow.ts +86 -0
  367. package/cli/sync/release.constants.ts +93 -0
  368. package/cli/sync/release.test.ts +187 -0
  369. package/cli/sync/release.ts +112 -0
  370. package/cli/sync/repo-dir.test.ts +371 -0
  371. package/cli/sync/repo-dir.ts +205 -0
  372. package/cli/sync/standards.constants.ts +8 -0
  373. package/cli/sync/standards.test.ts +206 -0
  374. package/cli/sync/standards.ts +179 -0
  375. package/cli/sync/stock-history.json +2796 -0
  376. package/cli/sync/stock-history.test.ts +66 -0
  377. package/cli/sync/stock-history.ts +84 -0
  378. package/cli/sync/stock.test.ts +47 -0
  379. package/cli/sync/stock.ts +24 -0
  380. package/cli/sync/trees.test.ts +621 -0
  381. package/cli/sync/trees.ts +280 -0
  382. package/cli/sync/validate.test.ts +299 -0
  383. package/cli/sync/validate.ts +402 -0
  384. package/cli/sync/workspace.test.ts +180 -0
  385. package/cli/sync/workspace.ts +138 -0
  386. package/cli/sync/write.test.ts +341 -0
  387. package/cli/sync/write.ts +231 -0
  388. package/cli/sync.test.ts +1495 -0
  389. package/cli/sync.ts +360 -0
  390. package/cli/tdd-scope.test.ts +36 -0
  391. package/cli/tdd-scope.ts +41 -0
  392. package/cli/test-files.ts +16 -0
  393. package/cli/ts-program.ts +69 -0
  394. package/cli/tsconfig.constants.ts +8 -0
  395. package/cli/workerstack-config.types.ts +21 -0
  396. package/dist/cli/env.d.ts +10 -0
  397. package/dist/cli/env.js +64 -0
  398. package/dist/presets/temp-root-env.d.ts +10 -0
  399. package/dist/presets/temp-root-env.js +20 -0
  400. package/dist/presets/temp-root.constants.d.ts +6 -0
  401. package/dist/presets/temp-root.constants.js +6 -0
  402. package/dist/presets/temp-root.d.ts +2 -0
  403. package/dist/presets/temp-root.js +60 -0
  404. package/dist/presets/vitest.constants.d.ts +16 -0
  405. package/dist/presets/vitest.constants.js +16 -0
  406. package/dist/presets/vitest.d.ts +11 -0
  407. package/dist/presets/vitest.js +67 -0
  408. package/dist/presets/workers-plugin.d.ts +10 -0
  409. package/dist/presets/workers-plugin.js +54 -0
  410. package/package.json +141 -4
  411. package/references/INDEX.md +100 -0
  412. package/references/browser.md +66 -0
  413. package/references/cascade.md +15 -0
  414. package/references/clasp.md +21 -0
  415. package/references/cloudflare.md +36 -0
  416. package/references/google-workspace.md +79 -0
  417. package/references/hooks.md +103 -0
  418. package/references/implementation-format.md +119 -0
  419. package/references/jev.md +56 -0
  420. package/references/lessons.md +73 -0
  421. package/references/listen.md +80 -0
  422. package/references/map.md +107 -0
  423. package/references/output.md +47 -0
  424. package/references/plan-format.md +264 -0
  425. package/references/review-loop.md +48 -0
  426. package/references/roadmap-format.md +104 -0
  427. package/references/rtk.md +27 -0
  428. package/references/scope.md +76 -0
  429. package/references/setup.md +42 -0
  430. package/references/speak.md +59 -0
  431. package/tsconfig/base.json +28 -0
  432. package/tsconfig/bun.d.ts +1 -0
  433. package/tsconfig/bun.json +8 -0
  434. package/tsconfig/reset.d.ts +1 -0
  435. package/tsconfig/strict.json +12 -0
  436. package/tsconfig/web.json +7 -0
  437. package/tsconfig/worker.d.ts +1 -0
  438. package/tsconfig/worker.json +8 -0
@@ -0,0 +1,934 @@
1
+ #!/usr/bin/env bun
2
+ /** Deterministic repo for job: search|exec|test|slice|fetch. Exit 2 + FALLBACK on denylist/HTML-as-exec/unbounded. */
3
+ import { spawnSync } from "node:child_process";
4
+ import crypto from "node:crypto";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { canonicalPath, resolveRoots } from "./workspace-root.ts";
8
+ import { childEnv } from "../env.ts";
9
+
10
+ export const FALLBACK = "FALLBACK";
11
+ export const SHELL_META: RegExp = /[|&;`$(){}<>\n\r]/;
12
+ export const MAX_FACTS = 6;
13
+ export const MAX_STDOUT: number = 32 * 1024;
14
+ export const STASH_CAP: number = 2 * 1024 * 1024;
15
+ export const MAX_HITS = 10_000;
16
+ export const STASH_REL = ".tmp/repo/stash";
17
+
18
+ const DENY_SEGS = new Set(["node_modules", "dist", ".next", "build", "coverage"]);
19
+ const DENY_LOCK = /\bbun\.lock\b/;
20
+ const FAIL_RE = /\b(FAIL(?:ED)?|ERROR|Error:|Exception|panic|E\d{3,})\b/;
21
+ const STACK_RE = /^\s+at\s/;
22
+ const PASS_RE = /^\s*(✓|√|PASS|ok |passed)\b/i;
23
+ const KNOWN_FLAGS = new Set(["--job", "--query", "--path", "--glob", "--cwd", "--stash", "--from", "--to"]);
24
+ const JOBS = new Set(["search", "exec", "test", "slice", "fetch"]);
25
+
26
+ /** What a job prints and the exit code it ends with; `fallback` tells the caller to run the plain command instead. */
27
+ export interface JobResult {
28
+ fallback: boolean;
29
+ text: string;
30
+ code: number;
31
+ }
32
+
33
+ /** The parsed `job.ts` command line. */
34
+ export interface ParsedArgs {
35
+ job: string;
36
+ query: string;
37
+ path: string;
38
+ glob: string[];
39
+ cwd: string;
40
+ stash: string;
41
+ from: string;
42
+ to: string;
43
+ unknown: boolean;
44
+ }
45
+
46
+ /** The facts `formatFacts` prints as the short verdict of one job. */
47
+ export interface FactsInput {
48
+ kind?: string;
49
+ n?: number;
50
+ facts?: string[];
51
+ errors?: string[];
52
+ failed?: boolean;
53
+ stash?: string;
54
+ truncated?: boolean;
55
+ }
56
+
57
+ /** Where a job runs (`cwd`) and where its stash lives (`root`). */
58
+ export interface StashRoots {
59
+ cwd?: string | undefined;
60
+ root?: string | undefined;
61
+ }
62
+
63
+ export interface SearchOptions extends StashRoots {
64
+ path?: string | undefined;
65
+ glob?: string[] | string | undefined;
66
+ }
67
+
68
+ export interface SliceOptions extends StashRoots {
69
+ stash?: string | undefined;
70
+ path?: string | undefined;
71
+ from?: string | number | null | undefined;
72
+ to?: string | number | null | undefined;
73
+ }
74
+
75
+ export interface JobOptions extends SearchOptions, SliceOptions {
76
+ job?: string | undefined;
77
+ query?: string | undefined;
78
+ unknown?: boolean | undefined;
79
+ }
80
+
81
+ interface SqueezeItem {
82
+ l: string;
83
+ i: number;
84
+ }
85
+
86
+ interface VerdictInput {
87
+ kind: string;
88
+ lines: string[];
89
+ raw: string;
90
+ query: string;
91
+ cwd: string | undefined;
92
+ root: string | undefined;
93
+ /** Set for a failed command: these lines are the verdict's errors. */
94
+ errors?: string[];
95
+ truncated?: boolean;
96
+ }
97
+
98
+ function isErrnoException(e: unknown): e is NodeJS.ErrnoException {
99
+ return e instanceof Error && "code" in e;
100
+ }
101
+
102
+ function takeFlag(argv: string[], i: number, prefix: string): { value: string; next: number } | null {
103
+ const a = argv[i]!; // callers loop i < argv.length
104
+ if (a === prefix) return { value: String(argv[i + 1] || ""), next: i + 1 };
105
+ if (a.startsWith(prefix + "=")) return { value: a.slice(prefix.length + 1), next: i };
106
+ return null;
107
+ }
108
+
109
+ export function parseArgv(argv: string[]): ParsedArgs {
110
+ let job = "";
111
+ let query = "";
112
+ let searchPath = "";
113
+ let cwd = "";
114
+ let stash = "";
115
+ let from = "";
116
+ let to = "";
117
+ const glob: string[] = [];
118
+ let unknown = false;
119
+ for (let i = 0; i < argv.length; i++) {
120
+ const a = argv[i]!; // i < argv.length
121
+ const prefix = a.startsWith("--") ? a.split("=")[0] : "";
122
+ if (prefix && !KNOWN_FLAGS.has(prefix)) {
123
+ unknown = true;
124
+ continue;
125
+ }
126
+ const jobF = takeFlag(argv, i, "--job");
127
+ if (jobF) {
128
+ job = jobF.value;
129
+ i = jobF.next;
130
+ continue;
131
+ }
132
+ const queryF = takeFlag(argv, i, "--query");
133
+ if (queryF) {
134
+ query = queryF.value;
135
+ i = queryF.next;
136
+ continue;
137
+ }
138
+ const pathF = takeFlag(argv, i, "--path");
139
+ if (pathF) {
140
+ searchPath = pathF.value;
141
+ i = pathF.next;
142
+ continue;
143
+ }
144
+ const cwdF = takeFlag(argv, i, "--cwd");
145
+ if (cwdF) {
146
+ cwd = cwdF.value;
147
+ i = cwdF.next;
148
+ continue;
149
+ }
150
+ const globF = takeFlag(argv, i, "--glob");
151
+ if (globF) {
152
+ if (globF.value) glob.push(globF.value);
153
+ i = globF.next;
154
+ continue;
155
+ }
156
+ const stashF = takeFlag(argv, i, "--stash");
157
+ if (stashF) {
158
+ stash = stashF.value;
159
+ i = stashF.next;
160
+ continue;
161
+ }
162
+ const fromF = takeFlag(argv, i, "--from");
163
+ if (fromF) {
164
+ from = fromF.value;
165
+ i = fromF.next;
166
+ continue;
167
+ }
168
+ const toF = takeFlag(argv, i, "--to");
169
+ if (toF) {
170
+ to = toF.value;
171
+ i = toF.next;
172
+ continue;
173
+ }
174
+ unknown = true;
175
+ }
176
+ return {
177
+ job: String(job).trim().toLowerCase(),
178
+ query: String(query),
179
+ path: String(searchPath),
180
+ glob,
181
+ cwd: String(cwd),
182
+ stash: String(stash).trim(),
183
+ from: String(from).trim(),
184
+ to: String(to).trim(),
185
+ unknown,
186
+ };
187
+ }
188
+
189
+ export function resolveUnder(root: string | undefined, rel?: string | null): string | null {
190
+ const base = canonicalPath(path.resolve(root || process.cwd()));
191
+ if (rel == null || rel === "") return base;
192
+ const abs = canonicalPath(path.resolve(base, rel));
193
+ const prefix = base.endsWith(path.sep) ? base : base + path.sep;
194
+ if (abs !== base && !abs.startsWith(prefix)) return null;
195
+ return abs;
196
+ }
197
+
198
+ export function resolveJobCwd(cwd: string, workspaceRoot?: string): string | null {
199
+ return resolveUnder(workspaceRoot || process.cwd(), cwd);
200
+ }
201
+
202
+ function escapeRe(s: string): string {
203
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
204
+ }
205
+
206
+ export function shouldFallback(job: unknown, query: unknown, extra: { stash?: string | undefined } = {}): boolean {
207
+ const j = String(job || "")
208
+ .trim()
209
+ .toLowerCase();
210
+ if (!JOBS.has(j)) return true;
211
+ if (j === "slice") return !String(extra.stash || "").trim();
212
+ if (j === "fetch") return !/^https?:\/\//i.test(String(query || "").trim());
213
+ const q = String(query || "");
214
+ if (!q.trim()) return true;
215
+ if (/\b(curl|wget)\b/i.test(q)) return true;
216
+ if (/<\s*html\b/i.test(q) || /<\s*!DOCTYPE html/i.test(q)) return true;
217
+ if (DENY_LOCK.test(q)) return true;
218
+ if (/(^|[\s/])\.git(\/|$)/.test(q.replace(/\\/g, "/"))) return true;
219
+ for (const seg of DENY_SEGS) {
220
+ const re = new RegExp(`(^|[\\/\\s'"\`])${escapeRe(seg)}([\\/\\s'"\`]|$)`);
221
+ if (re.test(q)) return true;
222
+ }
223
+ return false;
224
+ }
225
+
226
+ export function isHtml(stdout: unknown, stderr: unknown): boolean {
227
+ const s = String(stdout || "") + String(stderr || "");
228
+ return /<\s*html\b/i.test(s) || /<\s*!DOCTYPE html/i.test(s);
229
+ }
230
+
231
+ export function looksHtmlOrUnbounded(stdout: unknown, stderr: unknown): boolean {
232
+ return String(stdout || "").length + String(stderr || "").length > MAX_STDOUT || isHtml(stdout, stderr);
233
+ }
234
+
235
+ export function squeezLines(lines: unknown[] | null | undefined, query: unknown): string[] {
236
+ const arr: SqueezeItem[] = (lines || []).map((l, i) => ({ l: String(l), i }));
237
+ if (arr.length <= MAX_FACTS) return arr.map((x) => x.l);
238
+ const q = String(query || "").trim();
239
+ let queryRe: RegExp | null = null;
240
+ if (q) {
241
+ try {
242
+ queryRe = new RegExp(q);
243
+ } catch {
244
+ queryRe = null;
245
+ }
246
+ }
247
+ function score(item: SqueezeItem): number {
248
+ const s = item.l;
249
+ if (FAIL_RE.test(s) || STACK_RE.test(s)) return 100;
250
+ if (queryRe) {
251
+ const hit = queryRe.test(s);
252
+ queryRe.lastIndex = 0;
253
+ if (hit) return 50;
254
+ } else if (q && s.includes(q)) return 50;
255
+ if (item.i === 0 || item.i === arr.length - 1) return 20;
256
+ if (PASS_RE.test(s)) return 0;
257
+ return 10;
258
+ }
259
+ const ranked = arr.map((x) => ({ ...x, sc: score(x) }));
260
+ const kept: (SqueezeItem & { sc: number })[] = [];
261
+ const used = new Set<number>();
262
+ function take(pred: (x: SqueezeItem & { sc: number }) => boolean): void {
263
+ for (const x of ranked) {
264
+ if (kept.length >= MAX_FACTS) break;
265
+ if (used.has(x.i)) continue;
266
+ if (pred(x)) {
267
+ kept.push(x);
268
+ used.add(x.i);
269
+ }
270
+ }
271
+ }
272
+ take((x) => x.sc >= 100);
273
+ take((x) => x.sc >= 50);
274
+ if (!used.has(0) && kept.length < MAX_FACTS) {
275
+ kept.push(ranked[0]!); // arr has more than MAX_FACTS items
276
+ used.add(0);
277
+ }
278
+ const last = arr.length - 1;
279
+ if (!used.has(last) && kept.length < MAX_FACTS) {
280
+ kept.push(ranked[last]!); // last is a valid index
281
+ used.add(last);
282
+ }
283
+ take((x) => x.sc > 0);
284
+ take(() => true);
285
+ kept.sort((a, b) => a.i - b.i);
286
+ return kept.map((x) => x.l);
287
+ }
288
+
289
+ export function formatFacts({
290
+ kind,
291
+ n = 0,
292
+ facts,
293
+ errors,
294
+ failed = false,
295
+ stash = "",
296
+ truncated = false,
297
+ }: FactsInput = {}): string {
298
+ const lines: string[] = [];
299
+ const shown = (facts || []).slice(0, MAX_FACTS);
300
+ if (failed) {
301
+ const head = kind === "search" ? "Search failed." : "Command failed.";
302
+ lines.push(stash ? `${head.slice(0, -1)}. stash=${stash}` : head);
303
+ for (const e of (errors || []).slice(0, 3)) lines.push(`- ${e}`);
304
+ return lines.slice(0, 8).join("\n") + "\n";
305
+ }
306
+ if (!n) {
307
+ lines.push(kind === "search" ? "No matches." : "No output.");
308
+ return lines.join("\n") + "\n";
309
+ }
310
+ const unit = kind === "search" ? (n === 1 ? "match" : "matches") : n === 1 ? "line" : "lines";
311
+ const nLabel = truncated ? `${n}+` : String(n);
312
+ if (stash) {
313
+ lines.push(`${nLabel} ${unit}, ${shown.length} shown. stash=${stash}`);
314
+ } else if (kind === "search") {
315
+ lines.push(n === 1 ? "1 match." : `${n} matches.`);
316
+ } else {
317
+ lines.push(n === 1 ? "1 line." : `${n} lines.`);
318
+ }
319
+ for (const f of shown) lines.push(`- ${f}`);
320
+ return lines.slice(0, 8).join("\n") + "\n";
321
+ }
322
+
323
+ function stashId(): string {
324
+ const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
325
+ const b = crypto.randomBytes(4);
326
+ let s = "";
327
+ for (const x of b) s += alphabet[x % 36]!; // x % 36 is in range
328
+ return s;
329
+ }
330
+
331
+ function stashDir(root: string): string {
332
+ return path.join(root, ...STASH_REL.split("/"));
333
+ }
334
+
335
+ /** The stash path to print: relative from the root, absolute from anywhere else. */
336
+ function stashRef({ root, cwd }: { root: string; cwd: string }, name: string): string {
337
+ return path.resolve(cwd) === path.resolve(root) ? `${STASH_REL}/${name}` : path.join(stashDir(root), name);
338
+ }
339
+
340
+ export function writeStash(
341
+ { cwd = process.cwd(), root = resolveRoots({}, { cwd }).root }: StashRoots | undefined = {},
342
+ kind: string,
343
+ raw: unknown,
344
+ ): string {
345
+ const dir = stashDir(root);
346
+ fs.mkdirSync(dir, { recursive: true });
347
+ const name = `${kind}-${stashId()}.txt`;
348
+ let body = String(raw || "");
349
+ const buf = Buffer.from(body, "utf8");
350
+ if (buf.length > STASH_CAP) body = buf.subarray(0, STASH_CAP).toString("utf8");
351
+ if (!body.endsWith("\n")) body += "\n";
352
+ fs.writeFileSync(path.join(dir, name), body);
353
+ return stashRef({ root, cwd }, name);
354
+ }
355
+
356
+ /** A bare id or name, `.tmp/repo/stash/<name>`, or that file's absolute path, all in the root stash. */
357
+ export function resolveStashFile(
358
+ { cwd = process.cwd(), root = resolveRoots({}, { cwd }).root }: StashRoots | undefined = {},
359
+ stash: unknown,
360
+ ): string | null {
361
+ const id = String(stash || "").trim();
362
+ if (!id) return null;
363
+ const dir = canonicalPath(stashDir(root));
364
+ const named = path.isAbsolute(id)
365
+ ? id
366
+ : id.replace(/\\/g, "/").startsWith(`${STASH_REL}/`)
367
+ ? path.join(root, id)
368
+ : null;
369
+ if (named) {
370
+ const abs = canonicalPath(path.resolve(named));
371
+ return path.dirname(abs) === dir && fs.existsSync(abs) && fs.statSync(abs).isFile() ? abs : null;
372
+ }
373
+ if (id.includes("..") || /[\\/]/.test(id)) return null;
374
+ if (!fs.existsSync(dir)) return null;
375
+ const names = fs.readdirSync(dir);
376
+ const exact = names.find((n) => n === id || n === `${id}.txt`);
377
+ if (exact) return path.join(dir, exact);
378
+ const hit = names.find((n) => n.includes(id) && n.endsWith(".txt"));
379
+ return hit ? path.join(dir, hit) : null;
380
+ }
381
+
382
+ function verdictFromLines({ kind, lines, raw, query, cwd, root, errors, truncated = false }: VerdictInput): JobResult {
383
+ const n = lines.length;
384
+ const shown = squeezLines(lines, query);
385
+ let stash = "";
386
+ if (n > MAX_FACTS || Buffer.byteLength(raw, "utf8") > MAX_STDOUT) {
387
+ stash = writeStash({ cwd, root }, kind, raw);
388
+ }
389
+ if (errors) {
390
+ return {
391
+ fallback: false,
392
+ text: formatFacts({ kind, failed: true, errors: errors.slice(0, 3), stash, n, facts: shown }),
393
+ code: 0,
394
+ };
395
+ }
396
+ return {
397
+ fallback: false,
398
+ text: formatFacts({ kind, n, facts: shown, stash, truncated }),
399
+ code: 0,
400
+ };
401
+ }
402
+
403
+ /**
404
+ * A run that failed to finish (killed for output or time) leaves its process tree alive on Windows: spawnSync ends only
405
+ * the shell shim, and the shim's child would keep the run's cwd open. Ends the tree there and says so; false elsewhere.
406
+ */
407
+ export function endTreeAfterError(
408
+ run: { error?: Error | undefined; pid?: number | undefined },
409
+ platform: NodeJS.Platform = process.platform,
410
+ ): boolean {
411
+ if (!run.error || platform !== "win32" || !run.pid) return false;
412
+ spawnSync("taskkill", ["/PID", String(run.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
413
+ return true;
414
+ }
415
+
416
+ function runCmd(file: string, args: string[], cwd?: string) {
417
+ const run = spawnSync(file, args, {
418
+ encoding: "utf8",
419
+ cwd: cwd || process.cwd(),
420
+ timeout: 20_000,
421
+ maxBuffer: STASH_CAP + 4096,
422
+ env: childEnv(),
423
+ });
424
+ endTreeAfterError(run);
425
+ return run;
426
+ }
427
+
428
+ const FETCH_TIMEOUT_MS = 20_000;
429
+ const SEARCH_SKIP_DIRS = new Set(["node_modules", "dist", ".next", "build", "coverage", ".git"]);
430
+ const SEARCH_MAX_FILE = 200 * 1024;
431
+
432
+ function parseSearchHits(stdout: string): { n: number; facts: string[]; raw: string } {
433
+ const hits = stdout
434
+ .split(/\n/)
435
+ .map((l) => l.trim())
436
+ .filter(Boolean);
437
+ const facts = hits.map((line) => {
438
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
439
+ if (!m) return line.slice(0, 160);
440
+ return `${m[1]}:${m[2]} — ${m[3]!.trim().slice(0, 120)}`; // groups 1-3 always match
441
+ });
442
+ return { n: hits.length, facts, raw: hits.join("\n") };
443
+ }
444
+
445
+ export function globToRegExp(glob: unknown): RegExp {
446
+ const g = String(glob || "").replace(/\\/g, "/");
447
+ let s = "";
448
+ for (let i = 0; i < g.length; i++) {
449
+ const c = g[i]!; // i < g.length
450
+ if (c === "*" && g[i + 1] === "*") {
451
+ if (g[i + 2] === "/") {
452
+ s += "(?:.*/)?";
453
+ i += 2;
454
+ } else {
455
+ s += ".*";
456
+ i += 1;
457
+ }
458
+ } else if (c === "*") s += "[^/]*";
459
+ else if (c === "?") s += "[^/]";
460
+ else s += escapeRe(c);
461
+ }
462
+ return new RegExp("^" + s + "$");
463
+ }
464
+
465
+ export function globMatch(rel: unknown, pattern: unknown): boolean {
466
+ const n = String(rel || "").replace(/\\/g, "/");
467
+ let p = String(pattern || "").replace(/\\/g, "/");
468
+ if (!p) return false;
469
+ if (!p.includes("/")) p = "**/" + p;
470
+ return globToRegExp(p).test(n) || globToRegExp(String(pattern).replace(/\\/g, "/")).test(n);
471
+ }
472
+
473
+ function fileMatchesGlobs(rel: string, globs: string[]): boolean {
474
+ if (!globs.length) return true;
475
+ const base = path.posix.basename(rel);
476
+ return globs.some((g) => globMatch(rel, g) || globMatch(base, g));
477
+ }
478
+
479
+ function nodeSearch(
480
+ query: string,
481
+ { cwd: root, start: origin, glob }: { cwd: string; start: string; glob: string[] },
482
+ ): { n: number; facts: string[]; truncated: boolean } {
483
+ let re: RegExp | null;
484
+ try {
485
+ re = new RegExp(query);
486
+ } catch {
487
+ re = null;
488
+ }
489
+ const facts: string[] = [];
490
+ let n = 0;
491
+ let bytes = 0;
492
+
493
+ // Callers check the limits before each file, so a file starts with room left.
494
+ function searchFile(abs: string): void {
495
+ const rel = path.relative(root, abs).replace(/\\/g, "/");
496
+ let text: string;
497
+ try {
498
+ const st = fs.statSync(abs);
499
+ if (!st.isFile() || st.size > SEARCH_MAX_FILE || !fileMatchesGlobs(rel, glob)) return;
500
+ text = fs.readFileSync(abs, "utf8");
501
+ } catch {
502
+ return;
503
+ }
504
+ if (text.includes("\0")) return;
505
+ const lines = text.split(/\n/);
506
+ for (let i = 0; i < lines.length; i++) {
507
+ const line = lines[i]!; // i < lines.length
508
+ const hit = re ? re.test(line) : line.includes(query);
509
+ if (re) re.lastIndex = 0;
510
+ if (!hit) continue;
511
+ n += 1;
512
+ const fact = `${rel}:${i + 1} — ${line.trim().slice(0, 120)}`;
513
+ facts.push(fact);
514
+ bytes += Buffer.byteLength(fact, "utf8") + 1;
515
+ if (n >= MAX_HITS || bytes >= STASH_CAP) return;
516
+ }
517
+ }
518
+
519
+ function walk(dir: string): void {
520
+ let ents: fs.Dirent[];
521
+ try {
522
+ ents = fs.readdirSync(dir, { withFileTypes: true });
523
+ } catch {
524
+ return;
525
+ }
526
+ for (const ent of ents) {
527
+ if (n >= MAX_HITS || bytes >= STASH_CAP) return;
528
+ if (SEARCH_SKIP_DIRS.has(ent.name)) continue;
529
+ const abs = path.join(dir, ent.name);
530
+ if (ent.isDirectory()) {
531
+ walk(abs);
532
+ continue;
533
+ }
534
+ if (!ent.isFile()) continue;
535
+ searchFile(abs);
536
+ }
537
+ }
538
+
539
+ let st: fs.Stats;
540
+ try {
541
+ st = fs.statSync(origin);
542
+ } catch {
543
+ return { n: 0, facts: [], truncated: false };
544
+ }
545
+ if (st.isFile()) searchFile(origin);
546
+ else walk(origin);
547
+ return { n, facts, truncated: n >= MAX_HITS || bytes >= STASH_CAP };
548
+ }
549
+
550
+ export function runSearch(query: string, { cwd, root, path: searchPath, glob }: SearchOptions = {}): JobResult {
551
+ const base = cwd || process.cwd();
552
+ const start = searchPath ? resolveUnder(base, searchPath) : base;
553
+ if (!start) {
554
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
555
+ }
556
+ const globs = Array.isArray(glob) ? glob.filter(Boolean) : glob ? [String(glob)] : [];
557
+ const relStart = path.relative(base, start) || ".";
558
+ // --hidden: .repo/ (lessons, overlay) and the generated host trees (.claude, .agents, .grok, .codex) are dot dirs; .gitignore still applies.
559
+ const rgArgs = [
560
+ "-n",
561
+ "--hidden",
562
+ "--max-count",
563
+ String(MAX_HITS),
564
+ "--max-filesize",
565
+ "200K",
566
+ "-g",
567
+ "!node_modules",
568
+ "-g",
569
+ "!dist",
570
+ "-g",
571
+ "!coverage",
572
+ "-g",
573
+ "!.next",
574
+ "-g",
575
+ "!build",
576
+ "-g",
577
+ "!.git",
578
+ ];
579
+ for (const g of globs) {
580
+ rgArgs.push("-g", g);
581
+ }
582
+ rgArgs.push("-e", query, relStart);
583
+ const r = runCmd("rg", rgArgs, base);
584
+ if (!(isErrnoException(r.error) && r.error.code === "ENOENT")) {
585
+ const stdout = String(r.stdout || "");
586
+ const stderr = String(r.stderr || "");
587
+ if (isHtml(stdout, stderr)) {
588
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
589
+ }
590
+ if (r.status === 1) {
591
+ return {
592
+ fallback: false,
593
+ text: formatFacts({ kind: "search", n: 0, facts: [] }),
594
+ code: 0,
595
+ };
596
+ }
597
+ if (r.status === 0) {
598
+ const parsed = parseSearchHits(stdout);
599
+ const truncated = parsed.n >= MAX_HITS;
600
+ return verdictFromLines({
601
+ kind: "search",
602
+ lines: parsed.facts,
603
+ raw: parsed.raw,
604
+ query,
605
+ cwd: base,
606
+ root,
607
+ truncated,
608
+ });
609
+ }
610
+ if (r.status != null) {
611
+ const errors = stderr
612
+ .split(/\n/)
613
+ .map((l) => l.trim())
614
+ .filter(Boolean);
615
+ return {
616
+ fallback: false,
617
+ text: formatFacts({
618
+ kind: "search",
619
+ failed: true,
620
+ errors: errors.length ? errors : [`rg exit ${r.status}`],
621
+ }),
622
+ code: 0,
623
+ };
624
+ }
625
+ }
626
+
627
+ const node = nodeSearch(query, { cwd: base, start, glob: globs });
628
+ return verdictFromLines({
629
+ kind: "search",
630
+ lines: node.facts,
631
+ raw: node.facts.join("\n"),
632
+ query,
633
+ cwd: base,
634
+ root,
635
+ truncated: node.truncated,
636
+ });
637
+ }
638
+
639
+ function alreadyRtk(file: string): boolean {
640
+ return file === "rtk" || file.endsWith("/rtk");
641
+ }
642
+
643
+ export function runExec(query: string, { cwd, root }: StashRoots = {}): JobResult {
644
+ const q = String(query).trim();
645
+ if (SHELL_META.test(q)) {
646
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
647
+ }
648
+ const [file, ...args] = q.split(/\s+/).filter(Boolean);
649
+ if (!file) {
650
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
651
+ }
652
+ let r: ReturnType<typeof runCmd>;
653
+ if (alreadyRtk(file)) {
654
+ r = runCmd(file, args, cwd);
655
+ } else {
656
+ r = runCmd("rtk", [file, ...args], cwd);
657
+ if (isErrnoException(r.error) && r.error.code === "ENOENT") {
658
+ r = runCmd(file, args, cwd);
659
+ }
660
+ }
661
+ if (isErrnoException(r.error) && r.error.code === "ENOENT") {
662
+ return {
663
+ fallback: false,
664
+ text: formatFacts({
665
+ kind: "exec",
666
+ failed: true,
667
+ errors: ["ENOENT"],
668
+ }),
669
+ code: 0,
670
+ };
671
+ }
672
+ const stdout = String(r.stdout || "");
673
+ const stderr = String(r.stderr || "");
674
+ if (isHtml(stdout, stderr)) {
675
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
676
+ }
677
+ if (r.status !== 0) {
678
+ const errors = stderr
679
+ .split(/\n/)
680
+ .map((l) => l.trim())
681
+ .filter(Boolean);
682
+ if (!errors.length) {
683
+ const fromOut = stdout
684
+ .split(/\n/)
685
+ .map((l) => l.trim())
686
+ .filter(Boolean);
687
+ errors.push(...fromOut);
688
+ }
689
+ const raw = [stdout, stderr].filter(Boolean).join("\n");
690
+ const lines = errors.length ? errors : [`exit ${r.status}`];
691
+ return verdictFromLines({
692
+ kind: "exec",
693
+ lines,
694
+ raw,
695
+ query: q,
696
+ cwd,
697
+ root,
698
+ errors: lines,
699
+ });
700
+ }
701
+ const lines = stdout
702
+ .split(/\n/)
703
+ .map((l) => l.trim())
704
+ .filter(Boolean);
705
+ if (!lines.length && !stderr.trim()) {
706
+ return {
707
+ fallback: false,
708
+ text: formatFacts({ kind: "exec", n: 0, facts: [] }),
709
+ code: 0,
710
+ };
711
+ }
712
+ const body = lines.length ? lines : [stderr.trim()];
713
+ return verdictFromLines({
714
+ kind: "exec",
715
+ lines: body,
716
+ raw: stdout || stderr,
717
+ query: q,
718
+ cwd,
719
+ root,
720
+ });
721
+ }
722
+
723
+ export function runSlice({
724
+ stash,
725
+ path: filter,
726
+ from,
727
+ to,
728
+ cwd = process.cwd(),
729
+ root = resolveRoots({}, { cwd }).root,
730
+ }: SliceOptions = {}): JobResult {
731
+ const abs = resolveStashFile({ cwd, root }, stash);
732
+ if (!abs) {
733
+ return {
734
+ fallback: false,
735
+ text: formatFacts({
736
+ kind: "exec",
737
+ failed: true,
738
+ errors: ["missing stash"],
739
+ }),
740
+ code: 0,
741
+ };
742
+ }
743
+ let text: string;
744
+ try {
745
+ text = fs.readFileSync(abs, "utf8");
746
+ } catch {
747
+ return {
748
+ fallback: false,
749
+ text: formatFacts({
750
+ kind: "exec",
751
+ failed: true,
752
+ errors: ["missing stash"],
753
+ }),
754
+ code: 0,
755
+ };
756
+ }
757
+ if (text.includes("\0")) {
758
+ return {
759
+ fallback: false,
760
+ text: formatFacts({
761
+ kind: "exec",
762
+ failed: true,
763
+ errors: ["binary stash"],
764
+ }),
765
+ code: 0,
766
+ };
767
+ }
768
+ const numbered = text.split(/\n/).map((l, i) => ({ n: i + 1, l }));
769
+ let picked = numbered;
770
+ const a = from !== "" && from != null ? Number(from) : NaN;
771
+ const b = to !== "" && to != null ? Number(to) : NaN;
772
+ if (Number.isFinite(a) || Number.isFinite(b)) {
773
+ const start = Number.isFinite(a) ? a : 1;
774
+ const end = Number.isFinite(b) ? b : numbered.length;
775
+ picked = numbered.filter((x) => x.n >= start && x.n <= end);
776
+ }
777
+ if (filter) picked = picked.filter((x) => x.l.includes(String(filter)));
778
+ const factLines = picked.map((x) => `stash:${x.n} — ${x.l.trim().slice(0, 120)}`);
779
+ if (picked.length <= MAX_FACTS) {
780
+ return {
781
+ fallback: false,
782
+ text: formatFacts({ kind: "exec", n: picked.length, facts: factLines }),
783
+ code: 0,
784
+ };
785
+ }
786
+ const shown = squeezLines(factLines, filter);
787
+ return {
788
+ fallback: false,
789
+ text: formatFacts({
790
+ kind: "exec",
791
+ n: picked.length,
792
+ facts: shown,
793
+ stash: stashRef({ root, cwd }, path.basename(abs)),
794
+ }),
795
+ code: 0,
796
+ };
797
+ }
798
+
799
+ function stripHtml(html: unknown): { title: string; text: string } {
800
+ const s = String(html || "");
801
+ const title = (s.match(/<title[^>]*>([\s\S]*?)<\/title>/i) || [])[1] || "";
802
+ const text = s
803
+ .replace(/<script[\s\S]*?<\/script>/gi, " ")
804
+ .replace(/<style[\s\S]*?<\/style>/gi, " ")
805
+ .replace(/<[^>]+>/g, " ")
806
+ .replace(/&nbsp;/g, " ")
807
+ .replace(/&amp;/g, "&")
808
+ .replace(/&lt;/g, "<")
809
+ .replace(/&gt;/g, ">")
810
+ .replace(/\s+/g, " ")
811
+ .trim();
812
+ return { title: String(title).replace(/\s+/g, " ").trim(), text };
813
+ }
814
+
815
+ export function runFetch(
816
+ query: string,
817
+ { cwd, root, timeoutMs = FETCH_TIMEOUT_MS }: StashRoots & { timeoutMs?: number } = {},
818
+ ): JobResult {
819
+ const url = String(query || "").trim();
820
+ if (!/^https?:\/\//i.test(url)) {
821
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
822
+ }
823
+ const script = `
824
+ const u = process.argv[1];
825
+ const cap = ${STASH_CAP};
826
+ fetch(u, { redirect: "follow" })
827
+ .then(async (r) => {
828
+ const buf = Buffer.from(await r.arrayBuffer());
829
+ process.stdout.write(buf.subarray(0, cap));
830
+ })
831
+ .catch((e) => {
832
+ process.stderr.write(String(e && e.message ? e.message : e));
833
+ process.exit(1);
834
+ });
835
+ `;
836
+ const r = spawnSync(process.execPath, ["-e", script, url], {
837
+ encoding: "utf8",
838
+ cwd: cwd || process.cwd(),
839
+ timeout: timeoutMs,
840
+ maxBuffer: STASH_CAP + 4096,
841
+ env: childEnv(),
842
+ });
843
+ if (r.status !== 0) {
844
+ const err = String(r.stderr || "fetch failed")
845
+ .split(/\n/)
846
+ .map((l) => l.trim())
847
+ .filter(Boolean);
848
+ return {
849
+ fallback: false,
850
+ text: formatFacts({ kind: "exec", failed: true, errors: err.slice(0, 3) }),
851
+ code: 0,
852
+ };
853
+ }
854
+ const raw = String(r.stdout || "");
855
+ const { title, text } = stripHtml(raw);
856
+ const lines: string[] = [];
857
+ if (title) lines.push(title);
858
+ for (const part of text.split(/(?<=\.)\s+/)) {
859
+ const p = part.trim();
860
+ if (p) lines.push(p.slice(0, 160));
861
+ }
862
+ const body = lines.length ? lines : ["(empty)"];
863
+ return verdictFromLines({
864
+ kind: "exec",
865
+ lines: body,
866
+ raw,
867
+ query: url,
868
+ cwd,
869
+ root,
870
+ truncated: Buffer.byteLength(raw, "utf8") >= STASH_CAP,
871
+ });
872
+ }
873
+
874
+ function fallbackResult(): JobResult {
875
+ return { fallback: true, text: FALLBACK + "\n", code: 2 };
876
+ }
877
+
878
+ export function runJob({
879
+ job,
880
+ query,
881
+ cwd,
882
+ root,
883
+ path: searchPath,
884
+ glob,
885
+ stash,
886
+ from,
887
+ to,
888
+ unknown,
889
+ }: JobOptions = {}): JobResult {
890
+ if (unknown) return fallbackResult();
891
+ if (shouldFallback(job, query, { stash })) return fallbackResult();
892
+ const at = cwd || process.cwd();
893
+ const stashRoot = root || resolveRoots({}, { cwd: at }).root;
894
+ // shouldFallback passed, so a search, fetch or exec job has a non-empty query.
895
+ const q = String(query);
896
+ if (job === "search") return runSearch(q, { cwd: at, root: stashRoot, path: searchPath, glob });
897
+ if (job === "slice") {
898
+ return runSlice({ stash, path: searchPath, from, to, cwd: at, root: stashRoot });
899
+ }
900
+ if (job === "fetch") return runFetch(q, { cwd: at, root: stashRoot });
901
+ return runExec(q, { cwd: at, root: stashRoot });
902
+ }
903
+
904
+ export function main(argv: string[]): number {
905
+ try {
906
+ const parsed = parseArgv(argv);
907
+ if (parsed.unknown) {
908
+ process.stdout.write(FALLBACK + "\n");
909
+ return 2;
910
+ }
911
+ const roots = resolveRoots();
912
+ const cwd = parsed.cwd ? resolveJobCwd(parsed.cwd, roots.cwd) : roots.cwd;
913
+ if (!cwd) {
914
+ process.stdout.write(FALLBACK + "\n");
915
+ return 2;
916
+ }
917
+ const result = runJob({
918
+ job: parsed.job,
919
+ query: parsed.query,
920
+ cwd,
921
+ root: roots.root,
922
+ path: parsed.path,
923
+ glob: parsed.glob,
924
+ stash: parsed.stash,
925
+ from: parsed.from,
926
+ to: parsed.to,
927
+ });
928
+ process.stdout.write(result.text);
929
+ return result.code;
930
+ } catch {
931
+ process.stdout.write(FALLBACK + "\n");
932
+ return 2;
933
+ }
934
+ }