all-hands-cli 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 (305) hide show
  1. package/.allhands/README.md +75 -0
  2. package/.allhands/agents/compounder.yaml +15 -0
  3. package/.allhands/agents/coordinator.yaml +17 -0
  4. package/.allhands/agents/documentor.yaml +15 -0
  5. package/.allhands/agents/e2e-test-planner.yaml +17 -0
  6. package/.allhands/agents/emergent.yaml +22 -0
  7. package/.allhands/agents/executor.yaml +14 -0
  8. package/.allhands/agents/ideation.yaml +11 -0
  9. package/.allhands/agents/initiative-steering.yaml +19 -0
  10. package/.allhands/agents/judge.yaml +13 -0
  11. package/.allhands/agents/planner.yaml +19 -0
  12. package/.allhands/agents/pr-reviewer.yaml +15 -0
  13. package/.allhands/docs.json +5 -0
  14. package/.allhands/docs.local.json +26 -0
  15. package/.allhands/flows/COMPOUNDING.md +203 -0
  16. package/.allhands/flows/COORDINATION.md +89 -0
  17. package/.allhands/flows/CORE.md +87 -0
  18. package/.allhands/flows/DOCUMENTATION.md +218 -0
  19. package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
  20. package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
  21. package/.allhands/flows/IDEATION_SCOPING.md +154 -0
  22. package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
  23. package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
  24. package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
  25. package/.allhands/flows/PR_REVIEWING.md +43 -0
  26. package/.allhands/flows/SPEC_PLANNING.md +216 -0
  27. package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
  28. package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
  29. package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
  30. package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
  31. package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
  32. package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
  33. package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
  34. package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
  35. package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
  36. package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
  37. package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
  38. package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
  39. package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
  40. package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
  41. package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
  42. package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
  43. package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
  44. package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
  45. package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
  46. package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
  47. package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
  48. package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
  49. package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
  50. package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
  51. package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
  52. package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
  53. package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
  54. package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
  55. package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
  56. package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
  57. package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
  58. package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
  59. package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
  60. package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
  61. package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
  62. package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
  63. package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
  64. package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
  65. package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
  66. package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
  67. package/.allhands/harness/ah +131 -0
  68. package/.allhands/harness/package-lock.json +5292 -0
  69. package/.allhands/harness/package.json +52 -0
  70. package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
  71. package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
  72. package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
  73. package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
  74. package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
  75. package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
  76. package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
  77. package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
  78. package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
  79. package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
  80. package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
  81. package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
  82. package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
  83. package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
  84. package/.allhands/harness/src/cli.ts +36 -0
  85. package/.allhands/harness/src/commands/complexity.ts +177 -0
  86. package/.allhands/harness/src/commands/context7.ts +202 -0
  87. package/.allhands/harness/src/commands/docs.ts +557 -0
  88. package/.allhands/harness/src/commands/hooks.ts +24 -0
  89. package/.allhands/harness/src/commands/index.ts +51 -0
  90. package/.allhands/harness/src/commands/knowledge.ts +382 -0
  91. package/.allhands/harness/src/commands/memories.ts +302 -0
  92. package/.allhands/harness/src/commands/notify.ts +61 -0
  93. package/.allhands/harness/src/commands/oracle.ts +158 -0
  94. package/.allhands/harness/src/commands/perplexity.ts +220 -0
  95. package/.allhands/harness/src/commands/planning.ts +245 -0
  96. package/.allhands/harness/src/commands/schema.ts +73 -0
  97. package/.allhands/harness/src/commands/skills.ts +128 -0
  98. package/.allhands/harness/src/commands/solutions.ts +353 -0
  99. package/.allhands/harness/src/commands/spawn.ts +158 -0
  100. package/.allhands/harness/src/commands/specs.ts +532 -0
  101. package/.allhands/harness/src/commands/tavily.ts +226 -0
  102. package/.allhands/harness/src/commands/tools.ts +579 -0
  103. package/.allhands/harness/src/commands/trace.ts +327 -0
  104. package/.allhands/harness/src/commands/tui.ts +960 -0
  105. package/.allhands/harness/src/commands/validate.ts +143 -0
  106. package/.allhands/harness/src/commands/validation-tools.ts +108 -0
  107. package/.allhands/harness/src/hooks/context.ts +1442 -0
  108. package/.allhands/harness/src/hooks/enforcement.ts +170 -0
  109. package/.allhands/harness/src/hooks/index.ts +54 -0
  110. package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
  111. package/.allhands/harness/src/hooks/notification.ts +104 -0
  112. package/.allhands/harness/src/hooks/observability.ts +551 -0
  113. package/.allhands/harness/src/hooks/session.ts +88 -0
  114. package/.allhands/harness/src/hooks/shared.ts +815 -0
  115. package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
  116. package/.allhands/harness/src/hooks/validation.ts +617 -0
  117. package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
  118. package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
  119. package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
  120. package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
  121. package/.allhands/harness/src/lib/base-command.ts +198 -0
  122. package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
  123. package/.allhands/harness/src/lib/compaction.ts +313 -0
  124. package/.allhands/harness/src/lib/ctags.ts +497 -0
  125. package/.allhands/harness/src/lib/docs-validation.ts +907 -0
  126. package/.allhands/harness/src/lib/event-loop.ts +662 -0
  127. package/.allhands/harness/src/lib/flows.ts +155 -0
  128. package/.allhands/harness/src/lib/git.ts +276 -0
  129. package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
  130. package/.allhands/harness/src/lib/knowledge.ts +810 -0
  131. package/.allhands/harness/src/lib/llm.ts +255 -0
  132. package/.allhands/harness/src/lib/mcp-client.ts +432 -0
  133. package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
  134. package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
  135. package/.allhands/harness/src/lib/notification.ts +115 -0
  136. package/.allhands/harness/src/lib/opencode/index.ts +70 -0
  137. package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
  138. package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
  139. package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
  140. package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
  141. package/.allhands/harness/src/lib/oracle.ts +926 -0
  142. package/.allhands/harness/src/lib/planning-utils.ts +150 -0
  143. package/.allhands/harness/src/lib/planning.ts +605 -0
  144. package/.allhands/harness/src/lib/pr-review.ts +225 -0
  145. package/.allhands/harness/src/lib/prompts.ts +522 -0
  146. package/.allhands/harness/src/lib/schema.ts +418 -0
  147. package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
  148. package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
  149. package/.allhands/harness/src/lib/session.ts +164 -0
  150. package/.allhands/harness/src/lib/specs.ts +348 -0
  151. package/.allhands/harness/src/lib/tldr.ts +829 -0
  152. package/.allhands/harness/src/lib/tmux.ts +1051 -0
  153. package/.allhands/harness/src/lib/trace-store.ts +714 -0
  154. package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
  155. package/.allhands/harness/src/mcp/_template.ts +47 -0
  156. package/.allhands/harness/src/mcp/filesystem.ts +33 -0
  157. package/.allhands/harness/src/mcp/index.ts +69 -0
  158. package/.allhands/harness/src/mcp/playwright.ts +34 -0
  159. package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
  160. package/.allhands/harness/src/schemas/docs.schema.json +44 -0
  161. package/.allhands/harness/src/schemas/settings.schema.json +214 -0
  162. package/.allhands/harness/src/tui/actions.ts +227 -0
  163. package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
  164. package/.allhands/harness/src/tui/index.ts +1574 -0
  165. package/.allhands/harness/src/tui/modal.ts +232 -0
  166. package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
  167. package/.allhands/harness/src/tui/status-pane.ts +434 -0
  168. package/.allhands/harness/tsconfig.json +22 -0
  169. package/.allhands/harness/vitest.config.ts +13 -0
  170. package/.allhands/pillars.md +33 -0
  171. package/.allhands/principles.md +88 -0
  172. package/.allhands/schemas/alignment.yaml +51 -0
  173. package/.allhands/schemas/documentation.yaml +10 -0
  174. package/.allhands/schemas/prompt.yaml +92 -0
  175. package/.allhands/schemas/skill.yaml +34 -0
  176. package/.allhands/schemas/solution.yaml +131 -0
  177. package/.allhands/schemas/spec.yaml +67 -0
  178. package/.allhands/schemas/validation-suite.yaml +49 -0
  179. package/.allhands/schemas/workflow.yaml +51 -0
  180. package/.allhands/settings.json +57 -0
  181. package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
  182. package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
  183. package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
  184. package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
  185. package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
  186. package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
  187. package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
  188. package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
  189. package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
  190. package/.allhands/validation/browser-automation.md +109 -0
  191. package/.allhands/validation/xcode-automation.md +195 -0
  192. package/.allhands/workflows/documentation.md +86 -0
  193. package/.allhands/workflows/investigation.md +81 -0
  194. package/.allhands/workflows/milestone.md +91 -0
  195. package/.allhands/workflows/optimization.md +85 -0
  196. package/.allhands/workflows/refactor.md +99 -0
  197. package/.allhands/workflows/triage.md +81 -0
  198. package/.claude/README.md +1 -0
  199. package/.claude/agents/explorer.md +10 -0
  200. package/.claude/agents/researcher.md +11 -0
  201. package/.claude/agents/task-runner.md +8 -0
  202. package/.claude/settings.json +231 -0
  203. package/.env.ai.example +7 -0
  204. package/.github/workflows/npm-publish.yml +69 -0
  205. package/.internal.json +45 -0
  206. package/.tldr/config.json +11 -0
  207. package/.tldrignore +90 -0
  208. package/CLAUDE.md +6 -0
  209. package/README.md +98 -0
  210. package/bin/sync-cli.js +7552 -0
  211. package/concerns.md +7 -0
  212. package/docs/README.md +41 -0
  213. package/docs/agents/README.md +24 -0
  214. package/docs/agents/agent-configuration-system.md +86 -0
  215. package/docs/agents/execution-agents.md +50 -0
  216. package/docs/agents/knowledge-agents.md +61 -0
  217. package/docs/agents/orchestration-agent.md +57 -0
  218. package/docs/agents/planning-agents.md +84 -0
  219. package/docs/agents/quality-review-agents.md +67 -0
  220. package/docs/agents/workflow-agent-orchestration.md +69 -0
  221. package/docs/flows/README.md +44 -0
  222. package/docs/flows/compounding.md +126 -0
  223. package/docs/flows/coordination.md +72 -0
  224. package/docs/flows/core-harness-integration.md +63 -0
  225. package/docs/flows/documentation-orchestration.md +98 -0
  226. package/docs/flows/e2e-test-plan-building.md +83 -0
  227. package/docs/flows/emergent-refinement.md +104 -0
  228. package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
  229. package/docs/flows/judge-reviewing.md +112 -0
  230. package/docs/flows/plan-deepening-and-research.md +107 -0
  231. package/docs/flows/plan-review-jury.md +114 -0
  232. package/docs/flows/pr-reviewing.md +54 -0
  233. package/docs/flows/prompt-task-execution.md +119 -0
  234. package/docs/flows/spec-planning.md +162 -0
  235. package/docs/flows/type-specific-scoping-flows.md +49 -0
  236. package/docs/flows/validation-and-skills-integration.md +145 -0
  237. package/docs/flows/wip/wip-flows.md +102 -0
  238. package/docs/harness/README.md +23 -0
  239. package/docs/harness/agent-profiles.md +84 -0
  240. package/docs/harness/cli/README.md +24 -0
  241. package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
  242. package/docs/harness/cli/docs-command.md +87 -0
  243. package/docs/harness/cli/knowledge-command.md +91 -0
  244. package/docs/harness/cli/minor-cli-commands.md +65 -0
  245. package/docs/harness/cli/oracle-command.md +113 -0
  246. package/docs/harness/cli/planning-command.md +95 -0
  247. package/docs/harness/cli/schema-and-validation-commands.md +154 -0
  248. package/docs/harness/cli/search-commands.md +97 -0
  249. package/docs/harness/cli/spawn-command.md +136 -0
  250. package/docs/harness/cli/specs-command.md +102 -0
  251. package/docs/harness/cli/tools-command.md +122 -0
  252. package/docs/harness/cli/trace-command.md +122 -0
  253. package/docs/harness/cli-daemon.md +92 -0
  254. package/docs/harness/event-loop.md +184 -0
  255. package/docs/harness/hooks/README.md +15 -0
  256. package/docs/harness/hooks/context-hooks.md +96 -0
  257. package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
  258. package/docs/harness/hooks/validation-hooks.md +97 -0
  259. package/docs/harness/test-harness.md +149 -0
  260. package/docs/harness/tui.md +176 -0
  261. package/docs/memories.md +20 -0
  262. package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
  263. package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
  264. package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
  265. package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
  266. package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
  267. package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
  268. package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
  269. package/docs/sync-cli/README.md +19 -0
  270. package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
  271. package/docs/sync-cli/commands/README.md +11 -0
  272. package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
  273. package/docs/sync-cli/commands/push-command.md +84 -0
  274. package/docs/sync-cli/commands/sync-command.md +71 -0
  275. package/docs/sync-cli/systems/README.md +14 -0
  276. package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
  277. package/docs/sync-cli/systems/interactive-ui.md +43 -0
  278. package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
  279. package/docs/sync-cli/systems/path-resolution.md +42 -0
  280. package/package.json +46 -0
  281. package/scripts/install-shim.sh +40 -0
  282. package/scripts/pre-pack.sh +25 -0
  283. package/specs/harness-maintenance-skill.spec.md +138 -0
  284. package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
  285. package/specs/sync-init-flag.spec.md +117 -0
  286. package/specs/unified-workflow-orchestration.spec.md +250 -0
  287. package/specs/validation-tooling-practice.spec.md +98 -0
  288. package/specs/workflow-domain-configuration.spec.md +265 -0
  289. package/src/commands/pull-manifest.ts +31 -0
  290. package/src/commands/push.ts +344 -0
  291. package/src/commands/sync.ts +289 -0
  292. package/src/lib/constants.ts +10 -0
  293. package/src/lib/dotfiles.ts +36 -0
  294. package/src/lib/fs-utils.ts +18 -0
  295. package/src/lib/gh.ts +40 -0
  296. package/src/lib/git.ts +63 -0
  297. package/src/lib/gitignore.ts +167 -0
  298. package/src/lib/manifest.ts +121 -0
  299. package/src/lib/marker-sync.ts +39 -0
  300. package/src/lib/paths.ts +38 -0
  301. package/src/lib/target-lines.ts +66 -0
  302. package/src/lib/ui.ts +78 -0
  303. package/src/sync-cli.ts +120 -0
  304. package/target-lines.json +23 -0
  305. package/tsconfig.json +20 -0
@@ -0,0 +1,289 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { basename, dirname, join, resolve } from 'path';
4
+ import { isGitRepo, getStagedFiles } from '../lib/git.js';
5
+ import { Manifest, filesAreDifferent } from '../lib/manifest.js';
6
+ import { getAllhandsRoot } from '../lib/paths.js';
7
+ import { ConflictResolution, askConflictResolution, confirm, getNextBackupPath } from '../lib/ui.js';
8
+ import { SYNC_CONFIG_FILENAME, SYNC_CONFIG_TEMPLATE } from '../lib/constants.js';
9
+ import { restoreDotfiles } from '../lib/dotfiles.js';
10
+ import { ensureTargetLines } from '../lib/target-lines.js';
11
+
12
+ const AH_SHIM_SCRIPT = `#!/bin/bash
13
+ # AllHands CLI shim - finds and executes project-local .allhands/harness/ah
14
+ # Installed by: npx all-hands sync
15
+
16
+ dir="$PWD"
17
+ while [ "$dir" != "/" ]; do
18
+ if [ -x "$dir/.allhands/harness/ah" ]; then
19
+ exec "$dir/.allhands/harness/ah" "$@"
20
+ fi
21
+ dir="$(dirname "$dir")"
22
+ done
23
+
24
+ echo "error: not in an all-hands project (no .allhands/harness/ah found)" >&2
25
+ echo "hint: run 'npx all-hands sync .' to initialize this project" >&2
26
+ exit 1
27
+ `;
28
+
29
+ function setupAhShim(): { installed: boolean; path: string | null; inPath: boolean } {
30
+ const localBin = join(homedir(), '.local', 'bin');
31
+ const shimPath = join(localBin, 'ah');
32
+
33
+ // Check if ~/.local/bin is in PATH
34
+ const pathEnv = process.env.PATH || '';
35
+ const inPath = pathEnv.split(':').some(p =>
36
+ p === localBin || p === join(homedir(), '.local/bin')
37
+ );
38
+
39
+ // Check if shim already exists and is current
40
+ if (existsSync(shimPath)) {
41
+ const existing = readFileSync(shimPath, 'utf-8');
42
+ if (existing.includes('.allhands/harness/ah')) {
43
+ return { installed: false, path: shimPath, inPath };
44
+ }
45
+ }
46
+
47
+ // Create ~/.local/bin if needed
48
+ mkdirSync(localBin, { recursive: true });
49
+
50
+ // Write the shim
51
+ writeFileSync(shimPath, AH_SHIM_SCRIPT, { mode: 0o755 });
52
+
53
+ return { installed: true, path: shimPath, inPath };
54
+ }
55
+
56
+ export async function cmdSync(target: string = '.', autoYes: boolean = false, init: boolean = false): Promise<number> {
57
+ const resolvedTarget = resolve(process.cwd(), target);
58
+ const allhandsRoot = getAllhandsRoot();
59
+
60
+ // Detect if this is a first-time init or an update
61
+ const targetAllhandsDir = join(resolvedTarget, '.allhands');
62
+ const isFirstTime = !existsSync(targetAllhandsDir);
63
+
64
+ if (isFirstTime) {
65
+ console.log(`Initializing allhands in: ${resolvedTarget}`);
66
+ } else {
67
+ console.log(`Updating allhands in: ${resolvedTarget}`);
68
+ }
69
+ console.log(`Source: ${allhandsRoot}`);
70
+
71
+ if (!existsSync(resolvedTarget)) {
72
+ console.error(`Error: Target directory does not exist: ${resolvedTarget}`);
73
+ return 1;
74
+ }
75
+
76
+ if (!isGitRepo(resolvedTarget)) {
77
+ console.error(`Warning: Target is not a git repository: ${resolvedTarget}`);
78
+ if (!autoYes) {
79
+ if (!(await confirm('Continue anyway?'))) {
80
+ console.log('Aborted.');
81
+ return 1;
82
+ }
83
+ }
84
+ }
85
+
86
+ // Update-only: Check for staged changes to managed files
87
+ if (!isFirstTime) {
88
+ const manifest = new Manifest(allhandsRoot);
89
+ const distributable = manifest.getDistributableFiles();
90
+ const staged = getStagedFiles(resolvedTarget);
91
+ const managedPaths = new Set(distributable);
92
+
93
+ const stagedConflicts = [...staged].filter(f => managedPaths.has(f));
94
+ if (stagedConflicts.length > 0) {
95
+ console.error('Error: Staged changes detected in managed files:');
96
+ for (const f of stagedConflicts.sort()) {
97
+ console.error(` - ${f}`);
98
+ }
99
+ console.error("\nRun 'git stash' or commit first.");
100
+ return 1;
101
+ }
102
+ }
103
+
104
+ // Load manifest for file-by-file sync
105
+ const manifest = new Manifest(allhandsRoot);
106
+ const distributable = manifest.getDistributableFiles();
107
+
108
+ // Filter out init-only files when --init is not set
109
+ if (!init) {
110
+ for (const relPath of [...distributable]) {
111
+ if (manifest.isInitOnly(relPath)) {
112
+ distributable.delete(relPath);
113
+ }
114
+ }
115
+ }
116
+
117
+ let copied = 0;
118
+ let created = 0;
119
+ let skipped = 0;
120
+ let resolution: ConflictResolution = 'overwrite';
121
+ const conflicts: string[] = [];
122
+ const deletedInSource: string[] = [];
123
+
124
+ // Detect conflicts and deleted files
125
+ for (const relPath of distributable) {
126
+ const sourceFile = join(allhandsRoot, relPath);
127
+ const targetFile = join(resolvedTarget, relPath);
128
+
129
+ if (!existsSync(sourceFile)) {
130
+ // Update-only: track deleted files
131
+ if (!isFirstTime && existsSync(targetFile)) {
132
+ deletedInSource.push(relPath);
133
+ }
134
+ continue;
135
+ }
136
+
137
+ if (existsSync(targetFile)) {
138
+ if (filesAreDifferent(sourceFile, targetFile)) {
139
+ conflicts.push(relPath);
140
+ }
141
+ }
142
+ }
143
+
144
+ // Handle conflicts
145
+ if (conflicts.length > 0) {
146
+ if (autoYes) {
147
+ resolution = 'overwrite';
148
+ console.log(`\nAuto-overwriting ${conflicts.length} conflicting files (--yes mode)`);
149
+ } else {
150
+ resolution = await askConflictResolution(conflicts);
151
+ if (resolution === 'cancel') {
152
+ console.log('Aborted. No changes made.');
153
+ return 1;
154
+ }
155
+ }
156
+
157
+ if (resolution === 'backup') {
158
+ console.log('\nCreating backups...');
159
+ for (const relPath of conflicts) {
160
+ const targetFile = join(resolvedTarget, relPath);
161
+ const bkPath = getNextBackupPath(targetFile);
162
+ copyFileSync(targetFile, bkPath);
163
+ console.log(` ${relPath} → ${basename(bkPath)}`);
164
+ }
165
+ }
166
+ }
167
+
168
+ // Copy files
169
+ console.log('\nCopying allhands files...');
170
+ console.log(`Found ${distributable.size} files to distribute`);
171
+
172
+ for (const relPath of [...distributable].sort()) {
173
+ const sourceFile = join(allhandsRoot, relPath);
174
+ const targetFile = join(resolvedTarget, relPath);
175
+
176
+ if (!existsSync(sourceFile)) continue;
177
+
178
+ mkdirSync(dirname(targetFile), { recursive: true });
179
+
180
+ if (existsSync(targetFile)) {
181
+ if (!filesAreDifferent(sourceFile, targetFile)) {
182
+ skipped++;
183
+ continue;
184
+ }
185
+ copyFileSync(sourceFile, targetFile);
186
+ copied++;
187
+ } else {
188
+ copyFileSync(sourceFile, targetFile);
189
+ created++;
190
+ }
191
+ }
192
+
193
+ // Restore dotfiles (gitignore → .gitignore, etc.)
194
+ restoreDotfiles(resolvedTarget);
195
+
196
+ // Update-only: Handle deleted files
197
+ if (!isFirstTime && deletedInSource.length > 0) {
198
+ console.log(`\n${deletedInSource.length} files removed from allhands source:`);
199
+ for (const f of deletedInSource) {
200
+ console.log(` - ${f}`);
201
+ }
202
+ const shouldDelete = autoYes || (await confirm('Delete these from target?'));
203
+ if (shouldDelete) {
204
+ for (const f of deletedInSource) {
205
+ const targetFile = join(resolvedTarget, f);
206
+ if (existsSync(targetFile)) {
207
+ unlinkSync(targetFile);
208
+ console.log(` Deleted: ${f}`);
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // Ensure target files have required lines (CLAUDE.md, .gitignore, .tldrignore)
215
+ console.log('\nSyncing target-lines...');
216
+ const targetLinesUpdated = ensureTargetLines(resolvedTarget, true);
217
+
218
+ // Copy .env.ai.example
219
+ const envExamples = ['.env.example', '.env.ai.example'];
220
+ for (const envExample of envExamples) {
221
+ const sourceEnv = join(allhandsRoot, envExample);
222
+ const targetEnv = join(resolvedTarget, envExample);
223
+
224
+ if (existsSync(sourceEnv)) {
225
+ console.log(`Copying ${envExample}`);
226
+ copyFileSync(sourceEnv, targetEnv);
227
+ }
228
+ }
229
+
230
+ // Init-only: Setup ah CLI shim in ~/.local/bin
231
+ let shimResult: { installed: boolean; path: string | null; inPath: boolean } | null = null;
232
+ if (isFirstTime) {
233
+ console.log('\nSetting up `ah` command...');
234
+ shimResult = setupAhShim();
235
+ if (shimResult.installed) {
236
+ console.log(` Installed shim to ${shimResult.path}`);
237
+ } else {
238
+ console.log(` Shim already installed at ${shimResult.path}`);
239
+ }
240
+ if (!shimResult.inPath) {
241
+ console.log(' Warning: ~/.local/bin is not in your PATH');
242
+ console.log(' Add this to your shell config (.zshrc/.bashrc):');
243
+ console.log(' export PATH="$HOME/.local/bin:$PATH"');
244
+ }
245
+ }
246
+
247
+ // Init-only: Offer to create sync config for push command
248
+ let syncConfigCreated = false;
249
+ if (isFirstTime) {
250
+ const syncConfigPath = join(resolvedTarget, SYNC_CONFIG_FILENAME);
251
+
252
+ if (existsSync(syncConfigPath)) {
253
+ console.log(`\n${SYNC_CONFIG_FILENAME} already exists - skipping`);
254
+ } else if (!autoYes) {
255
+ console.log('\nThe push command lets you contribute changes back to all-hands.');
256
+ console.log('A sync config file lets you customize which files to include/exclude.');
257
+ if (await confirm(`Create ${SYNC_CONFIG_FILENAME}?`)) {
258
+ writeFileSync(syncConfigPath, JSON.stringify(SYNC_CONFIG_TEMPLATE, null, 2) + '\n');
259
+ syncConfigCreated = true;
260
+ console.log(` Created ${SYNC_CONFIG_FILENAME}`);
261
+ }
262
+ }
263
+ }
264
+
265
+ // Summary
266
+ console.log(`\n${'='.repeat(60)}`);
267
+ if (isFirstTime) {
268
+ console.log(`Done: ${copied + created} copied, ${skipped} unchanged`);
269
+ } else {
270
+ console.log(`Updated: ${copied}, Created: ${created}`);
271
+ }
272
+ if (resolution === 'backup' && conflicts.length > 0) {
273
+ console.log(`Created ${conflicts.length} backup file(s)`);
274
+ }
275
+ if (targetLinesUpdated) {
276
+ console.log('Target files updated with required lines');
277
+ }
278
+ if (syncConfigCreated) {
279
+ console.log(`Created ${SYNC_CONFIG_FILENAME} for push customization`);
280
+ }
281
+ console.log(`${'='.repeat(60)}`);
282
+
283
+ if (isFirstTime) {
284
+ console.log('\nNext steps:');
285
+ console.log(' 1. Commit the changes');
286
+ }
287
+
288
+ return 0;
289
+ }
@@ -0,0 +1,10 @@
1
+ export const SYNC_CONFIG_FILENAME = '.allhands-sync-config.json';
2
+
3
+ // Files that should never be pushed back to upstream
4
+ export const PUSH_BLOCKLIST = ['CLAUDE.project.md', '.allhands-sync-config.json'];
5
+
6
+ export const SYNC_CONFIG_TEMPLATE = {
7
+ $comment: 'Customization for claude-all-hands push command',
8
+ includes: [],
9
+ excludes: [],
10
+ };
@@ -0,0 +1,36 @@
1
+ import { existsSync, renameSync } from 'fs';
2
+ import { basename, dirname, join } from 'path';
3
+ import { walkDir } from './fs-utils.js';
4
+
5
+ // Files that npm hardcode-excludes and we rename for packaging
6
+ const DOTFILE_NAMES = ['gitignore', 'npmrc', 'npmignore'];
7
+
8
+ /**
9
+ * Restore dotfiles after copying from npm package.
10
+ * Renames `gitignore` → `.gitignore`, `npmrc` → `.npmrc`, etc.
11
+ * Returns count of files renamed.
12
+ */
13
+ export function restoreDotfiles(targetDir: string): { renamed: string[]; skipped: string[] } {
14
+ const renamed: string[] = [];
15
+ const skipped: string[] = [];
16
+
17
+ walkDir(targetDir, (filePath) => {
18
+ const name = basename(filePath);
19
+
20
+ if (DOTFILE_NAMES.includes(name)) {
21
+ const dir = dirname(filePath);
22
+ const dotName = '.' + name;
23
+ const dotPath = join(dir, dotName);
24
+
25
+ if (existsSync(dotPath)) {
26
+ // Target dotfile already exists - skip to avoid overwriting
27
+ skipped.push(filePath);
28
+ } else {
29
+ renameSync(filePath, dotPath);
30
+ renamed.push(filePath);
31
+ }
32
+ }
33
+ });
34
+
35
+ return { renamed, skipped };
36
+ }
@@ -0,0 +1,18 @@
1
+ import { existsSync, readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function walkDir(dir: string, callback: (filePath: string) => void): void {
5
+ if (!existsSync(dir)) return;
6
+ const entries = readdirSync(dir, { withFileTypes: true });
7
+ for (const entry of entries) {
8
+ if (entry.name === '.git' || entry.name === 'node_modules') {
9
+ continue;
10
+ }
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ walkDir(fullPath, callback);
14
+ } else if (entry.isFile()) {
15
+ callback(fullPath);
16
+ }
17
+ }
18
+ }
package/src/lib/gh.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+
3
+ export interface GhResult {
4
+ success: boolean;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ export function gh(args: string[], cwd?: string): GhResult {
10
+ const result = spawnSync('gh', args, {
11
+ cwd: cwd || process.cwd(),
12
+ encoding: 'utf-8',
13
+ maxBuffer: 10 * 1024 * 1024,
14
+ });
15
+
16
+ return {
17
+ success: result.status === 0,
18
+ stdout: result.stdout?.trim() || '',
19
+ stderr: result.stderr?.trim() || '',
20
+ };
21
+ }
22
+
23
+ export function checkGhInstalled(): boolean {
24
+ try {
25
+ execSync('gh --version', { stdio: 'ignore' });
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ }
31
+
32
+ export function checkGhAuth(): boolean {
33
+ const result = gh(['auth', 'status']);
34
+ return result.success;
35
+ }
36
+
37
+ export function getGhUser(): string | null {
38
+ const result = gh(['api', 'user', '-q', '.login']);
39
+ return result.success ? result.stdout : null;
40
+ }
package/src/lib/git.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { execSync, spawnSync } from 'child_process';
2
+
3
+ export interface GitResult {
4
+ success: boolean;
5
+ stdout: string;
6
+ stderr: string;
7
+ }
8
+
9
+ export function git(args: string[], cwd: string): GitResult {
10
+ const result = spawnSync('git', args, {
11
+ cwd,
12
+ encoding: 'utf-8',
13
+ maxBuffer: 10 * 1024 * 1024,
14
+ });
15
+
16
+ return {
17
+ success: result.status === 0,
18
+ stdout: result.stdout?.trim() || '',
19
+ stderr: result.stderr?.trim() || '',
20
+ };
21
+ }
22
+
23
+ export function getStagedFiles(repoPath: string): Set<string> {
24
+ const result = git(['diff', '--cached', '--name-only'], repoPath);
25
+ if (!result.success || !result.stdout) {
26
+ return new Set();
27
+ }
28
+ return new Set(result.stdout.split('\n').filter(Boolean));
29
+ }
30
+
31
+ export function isGitRepo(path: string): boolean {
32
+ const result = git(['rev-parse', '--git-dir'], path);
33
+ return result.success;
34
+ }
35
+
36
+ export function checkGitInstalled(): boolean {
37
+ try {
38
+ execSync('git --version', { stdio: 'ignore' });
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Get all files tracked by git plus untracked files, excluding gitignored files.
47
+ * This respects .gitignore at all levels.
48
+ */
49
+ export function getGitFiles(repoPath: string): string[] {
50
+ // Get tracked files
51
+ const tracked = git(['ls-files'], repoPath);
52
+ // Get untracked files that are NOT ignored
53
+ const untracked = git(['ls-files', '--others', '--exclude-standard'], repoPath);
54
+
55
+ const files: string[] = [];
56
+ if (tracked.success && tracked.stdout) {
57
+ files.push(...tracked.stdout.split('\n').filter(Boolean));
58
+ }
59
+ if (untracked.success && untracked.stdout) {
60
+ files.push(...untracked.stdout.split('\n').filter(Boolean));
61
+ }
62
+ return files;
63
+ }
@@ -0,0 +1,167 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join, relative, dirname } from 'path';
3
+ import { minimatch } from 'minimatch';
4
+ import { walkDir } from './fs-utils.js';
5
+
6
+ interface GitignoreRule {
7
+ pattern: string;
8
+ negated: boolean;
9
+ directory: string; // Directory where the .gitignore lives (relative to root)
10
+ }
11
+
12
+ /**
13
+ * Parse a single .gitignore file and return rules.
14
+ */
15
+ function parseGitignoreFile(content: string, directory: string): GitignoreRule[] {
16
+ const rules: GitignoreRule[] = [];
17
+ const lines = content.split('\n');
18
+
19
+ for (const line of lines) {
20
+ const trimmed = line.trim();
21
+
22
+ // Skip empty lines and comments
23
+ if (!trimmed || trimmed.startsWith('#')) {
24
+ continue;
25
+ }
26
+
27
+ let pattern = trimmed;
28
+ let negated = false;
29
+
30
+ // Handle negation
31
+ if (pattern.startsWith('!')) {
32
+ negated = true;
33
+ pattern = pattern.slice(1);
34
+ }
35
+
36
+ // Remove trailing spaces (unless escaped)
37
+ pattern = pattern.replace(/(?<!\\)\s+$/, '');
38
+
39
+ // Skip empty patterns after processing
40
+ if (!pattern) continue;
41
+
42
+ rules.push({ pattern, negated, directory });
43
+ }
44
+
45
+ return rules;
46
+ }
47
+
48
+ /**
49
+ * Check if a file path matches a gitignore pattern.
50
+ * Handles directory-relative patterns correctly.
51
+ */
52
+ function matchesPattern(filePath: string, rule: GitignoreRule): boolean {
53
+ const { pattern, directory } = rule;
54
+
55
+ // Get the path relative to the gitignore's directory
56
+ let relativePath = filePath;
57
+ if (directory) {
58
+ if (!filePath.startsWith(directory + '/') && filePath !== directory) {
59
+ // File is not under this gitignore's directory
60
+ return false;
61
+ }
62
+ relativePath = filePath.slice(directory.length + 1);
63
+ }
64
+
65
+ // Handle patterns that should only match from root of gitignore dir
66
+ let matchPattern = pattern;
67
+
68
+ // Pattern starting with / means root-relative
69
+ if (pattern.startsWith('/')) {
70
+ matchPattern = pattern.slice(1);
71
+ }
72
+
73
+ // Pattern ending with / means directory only (we treat all as potential matches)
74
+ if (matchPattern.endsWith('/')) {
75
+ matchPattern = matchPattern.slice(0, -1) + '/**';
76
+ }
77
+
78
+ // If pattern has no slash, it can match at any level
79
+ // If pattern has slash (not just trailing), it's relative to gitignore location
80
+ const hasSlash = pattern.includes('/') && !pattern.endsWith('/');
81
+
82
+ if (!hasSlash && !pattern.startsWith('/')) {
83
+ // Match at any level: foo matches a/b/foo and foo
84
+ matchPattern = '**/' + matchPattern;
85
+ }
86
+
87
+ // Try matching
88
+ const opts = { dot: true, matchBase: false };
89
+
90
+ if (minimatch(relativePath, matchPattern, opts)) {
91
+ return true;
92
+ }
93
+
94
+ // Also try with ** suffix for directories
95
+ if (minimatch(relativePath, matchPattern + '/**', opts)) {
96
+ return true;
97
+ }
98
+
99
+ return false;
100
+ }
101
+
102
+ /**
103
+ * Collector class that gathers all .gitignore rules from a directory tree.
104
+ */
105
+ export class GitignoreFilter {
106
+ private rules: GitignoreRule[] = [];
107
+ private rootDir: string;
108
+
109
+ constructor(rootDir: string) {
110
+ this.rootDir = rootDir;
111
+ this.loadGitignoreFiles();
112
+ }
113
+
114
+ /**
115
+ * Walk the directory tree and load all .gitignore files.
116
+ */
117
+ private loadGitignoreFiles(): void {
118
+ // Check root .gitignore
119
+ const rootGitignore = join(this.rootDir, '.gitignore');
120
+ if (existsSync(rootGitignore)) {
121
+ const content = readFileSync(rootGitignore, 'utf-8');
122
+ this.rules.push(...parseGitignoreFile(content, ''));
123
+ }
124
+
125
+ // Walk and find nested .gitignore files
126
+ walkDir(this.rootDir, (filePath) => {
127
+ const relativePath = relative(this.rootDir, filePath);
128
+ if (relativePath.endsWith('.gitignore') && relativePath !== '.gitignore') {
129
+ const content = readFileSync(filePath, 'utf-8');
130
+ const directory = dirname(relativePath);
131
+ this.rules.push(...parseGitignoreFile(content, directory));
132
+ }
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Check if a file should be ignored based on all gitignore rules.
138
+ */
139
+ isIgnored(filePath: string): boolean {
140
+ let ignored = false;
141
+
142
+ // Process rules in order - later rules can override earlier ones
143
+ for (const rule of this.rules) {
144
+ if (matchesPattern(filePath, rule)) {
145
+ ignored = !rule.negated;
146
+ }
147
+ }
148
+
149
+ return ignored;
150
+ }
151
+
152
+ /**
153
+ * Get all non-ignored files from the directory tree.
154
+ */
155
+ getNonIgnoredFiles(): string[] {
156
+ const files: string[] = [];
157
+
158
+ walkDir(this.rootDir, (filePath) => {
159
+ const relativePath = relative(this.rootDir, filePath);
160
+ if (!this.isIgnored(relativePath)) {
161
+ files.push(relativePath);
162
+ }
163
+ });
164
+
165
+ return files;
166
+ }
167
+ }