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.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- 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
|
+
}
|