harnery 0.0.1 → 0.2.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/LICENSE +21 -0
- package/README.md +84 -2
- package/bin/agent-coord +42 -0
- package/bin/agent-hook +44 -0
- package/bin/harn +40 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +18 -0
- package/dist/commander.d.ts +128 -0
- package/dist/commander.d.ts.map +1 -0
- package/dist/commander.js +126 -0
- package/dist/commands/agents.d.ts +18 -0
- package/dist/commands/agents.d.ts.map +1 -0
- package/dist/commands/agents.js +3946 -0
- package/dist/commands/backup.d.ts +22 -0
- package/dist/commands/backup.d.ts.map +1 -0
- package/dist/commands/backup.js +262 -0
- package/dist/commands/browse-ai.d.ts +4 -0
- package/dist/commands/browse-ai.d.ts.map +1 -0
- package/dist/commands/browse-ai.js +156 -0
- package/dist/commands/browse.d.ts +4 -0
- package/dist/commands/browse.d.ts.map +1 -0
- package/dist/commands/browse.js +590 -0
- package/dist/commands/callers.d.ts +4 -0
- package/dist/commands/callers.d.ts.map +1 -0
- package/dist/commands/callers.js +276 -0
- package/dist/commands/completion.d.ts +17 -0
- package/dist/commands/completion.d.ts.map +1 -0
- package/dist/commands/completion.js +158 -0
- package/dist/commands/config-get.d.ts +4 -0
- package/dist/commands/config-get.d.ts.map +1 -0
- package/dist/commands/config-get.js +131 -0
- package/dist/commands/context.d.ts +11 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +185 -0
- package/dist/commands/cookies.d.ts +4 -0
- package/dist/commands/cookies.d.ts.map +1 -0
- package/dist/commands/cookies.js +140 -0
- package/dist/commands/docs.d.ts +4 -0
- package/dist/commands/docs.d.ts.map +1 -0
- package/dist/commands/docs.js +137 -0
- package/dist/commands/doctor.d.ts +25 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +200 -0
- package/dist/commands/edit-batch.d.ts +18 -0
- package/dist/commands/edit-batch.d.ts.map +1 -0
- package/dist/commands/edit-batch.js +172 -0
- package/dist/commands/eml.d.ts +4 -0
- package/dist/commands/eml.d.ts.map +1 -0
- package/dist/commands/eml.js +428 -0
- package/dist/commands/env.d.ts +4 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +201 -0
- package/dist/commands/fetch.d.ts +4 -0
- package/dist/commands/fetch.d.ts.map +1 -0
- package/dist/commands/fetch.js +99 -0
- package/dist/commands/file-history.d.ts +4 -0
- package/dist/commands/file-history.d.ts.map +1 -0
- package/dist/commands/file-history.js +152 -0
- package/dist/commands/grep.d.ts +4 -0
- package/dist/commands/grep.d.ts.map +1 -0
- package/dist/commands/grep.js +317 -0
- package/dist/commands/init.d.ts +82 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +288 -0
- package/dist/commands/outline.d.ts +4 -0
- package/dist/commands/outline.d.ts.map +1 -0
- package/dist/commands/outline.js +494 -0
- package/dist/commands/presence.d.ts +12 -0
- package/dist/commands/presence.d.ts.map +1 -0
- package/dist/commands/presence.js +123 -0
- package/dist/commands/read.d.ts +7 -0
- package/dist/commands/read.d.ts.map +1 -0
- package/dist/commands/read.js +46 -0
- package/dist/commands/scratch.d.ts +4 -0
- package/dist/commands/scratch.d.ts.map +1 -0
- package/dist/commands/scratch.js +426 -0
- package/dist/commands/session.d.ts +4 -0
- package/dist/commands/session.d.ts.map +1 -0
- package/dist/commands/session.js +162 -0
- package/dist/commands/sync.d.ts +24 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +275 -0
- package/dist/commands/toc.d.ts +5 -0
- package/dist/commands/toc.d.ts.map +1 -0
- package/dist/commands/toc.js +153 -0
- package/dist/commands/tokens.d.ts +4 -0
- package/dist/commands/tokens.d.ts.map +1 -0
- package/dist/commands/tokens.js +48 -0
- package/dist/commands/tunnel.d.ts +4 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +513 -0
- package/dist/commands/uninstall.d.ts +22 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +126 -0
- package/dist/commands/web.d.ts +4 -0
- package/dist/commands/web.d.ts.map +1 -0
- package/dist/commands/web.js +165 -0
- package/dist/core/agents/canonical-emit.d.ts +27 -0
- package/dist/core/agents/canonical-emit.d.ts.map +1 -0
- package/dist/core/agents/canonical-emit.js +72 -0
- package/dist/core/agents/cli-emit.d.ts +27 -0
- package/dist/core/agents/cli-emit.d.ts.map +1 -0
- package/dist/core/agents/cli-emit.js +57 -0
- package/dist/core/agents/cli.d.ts +10 -0
- package/dist/core/agents/cli.d.ts.map +1 -0
- package/dist/core/agents/cli.js +757 -0
- package/dist/core/agents/codex-replay.d.ts +29 -0
- package/dist/core/agents/codex-replay.d.ts.map +1 -0
- package/dist/core/agents/codex-replay.js +138 -0
- package/dist/core/agents/coord-client.d.ts +98 -0
- package/dist/core/agents/coord-client.d.ts.map +1 -0
- package/dist/core/agents/coord-client.js +212 -0
- package/dist/core/agents/events/consume.d.ts +59 -0
- package/dist/core/agents/events/consume.d.ts.map +1 -0
- package/dist/core/agents/events/consume.js +147 -0
- package/dist/core/agents/events/emit.d.ts +42 -0
- package/dist/core/agents/events/emit.d.ts.map +1 -0
- package/dist/core/agents/events/emit.js +70 -0
- package/dist/core/agents/events/ulid.d.ts +11 -0
- package/dist/core/agents/events/ulid.d.ts.map +1 -0
- package/dist/core/agents/events/ulid.js +47 -0
- package/dist/core/agents/index.d.ts +14 -0
- package/dist/core/agents/index.d.ts.map +1 -0
- package/dist/core/agents/index.js +13 -0
- package/dist/core/agents/paths.d.ts +6 -0
- package/dist/core/agents/paths.d.ts.map +1 -0
- package/dist/core/agents/paths.js +17 -0
- package/dist/core/agents/render/prompt-context.d.ts +43 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -0
- package/dist/core/agents/render/prompt-context.js +335 -0
- package/dist/core/agents/render/session-context.d.ts +39 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -0
- package/dist/core/agents/render/session-context.js +283 -0
- package/dist/core/agents/rules/claim-conflict.d.ts +35 -0
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/claim-conflict.js +244 -0
- package/dist/core/agents/rules/commit-conflict.d.ts +59 -0
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -0
- package/dist/core/agents/rules/commit-conflict.js +244 -0
- package/dist/core/agents/rules/stop-hook.d.ts +44 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -0
- package/dist/core/agents/rules/stop-hook.js +161 -0
- package/dist/core/agents/session-events.d.ts +41 -0
- package/dist/core/agents/session-events.d.ts.map +1 -0
- package/dist/core/agents/session-events.js +205 -0
- package/dist/core/agents/state/activity-log.d.ts +18 -0
- package/dist/core/agents/state/activity-log.d.ts.map +1 -0
- package/dist/core/agents/state/activity-log.js +34 -0
- package/dist/core/agents/state/council.d.ts +39 -0
- package/dist/core/agents/state/council.d.ts.map +1 -0
- package/dist/core/agents/state/council.js +216 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts +59 -0
- package/dist/core/agents/state/heartbeat-projector.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-projector.js +436 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts +64 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -0
- package/dist/core/agents/state/heartbeat-writer.js +271 -0
- package/dist/core/agents/state/names.d.ts +35 -0
- package/dist/core/agents/state/names.d.ts.map +1 -0
- package/dist/core/agents/state/names.js +376 -0
- package/dist/core/agents/state/pidmap.d.ts +11 -0
- package/dist/core/agents/state/pidmap.d.ts.map +1 -0
- package/dist/core/agents/state/pidmap.js +32 -0
- package/dist/core/agents/state/scratch.d.ts +27 -0
- package/dist/core/agents/state/scratch.d.ts.map +1 -0
- package/dist/core/agents/state/scratch.js +90 -0
- package/dist/core/agents/state/shell-mutation.d.ts +17 -0
- package/dist/core/agents/state/shell-mutation.d.ts.map +1 -0
- package/dist/core/agents/state/shell-mutation.js +41 -0
- package/dist/core/agents/state/stale-sweep.d.ts +16 -0
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -0
- package/dist/core/agents/state/stale-sweep.js +166 -0
- package/dist/core/config.d.ts +29 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +108 -0
- package/dist/core/hooks/cli.d.ts +21 -0
- package/dist/core/hooks/cli.d.ts.map +1 -0
- package/dist/core/hooks/cli.js +1123 -0
- package/dist/core/hooks/effects/image-capture.d.ts +43 -0
- package/dist/core/hooks/effects/image-capture.d.ts.map +1 -0
- package/dist/core/hooks/effects/image-capture.js +288 -0
- package/dist/core/hooks/effects/index.d.ts +64 -0
- package/dist/core/hooks/effects/index.d.ts.map +1 -0
- package/dist/core/hooks/effects/index.js +197 -0
- package/dist/core/hooks/events/emit.d.ts +31 -0
- package/dist/core/hooks/events/emit.d.ts.map +1 -0
- package/dist/core/hooks/events/emit.js +89 -0
- package/dist/core/hooks/events/schema.d.ts +235 -0
- package/dist/core/hooks/events/schema.d.ts.map +1 -0
- package/dist/core/hooks/events/schema.js +12 -0
- package/dist/core/hooks/events/ulid.d.ts +10 -0
- package/dist/core/hooks/events/ulid.d.ts.map +1 -0
- package/dist/core/hooks/events/ulid.js +47 -0
- package/dist/core/hooks/harness/detect.d.ts +9 -0
- package/dist/core/hooks/harness/detect.d.ts.map +1 -0
- package/dist/core/hooks/harness/detect.js +29 -0
- package/dist/core/hooks/harness/events.d.ts +45 -0
- package/dist/core/hooks/harness/events.d.ts.map +1 -0
- package/dist/core/hooks/harness/events.js +71 -0
- package/dist/core/hooks/harness/output.d.ts +46 -0
- package/dist/core/hooks/harness/output.d.ts.map +1 -0
- package/dist/core/hooks/harness/output.js +87 -0
- package/dist/core/hooks/harness/parse.d.ts +67 -0
- package/dist/core/hooks/harness/parse.d.ts.map +1 -0
- package/dist/core/hooks/harness/parse.js +132 -0
- package/dist/core/hooks/index.d.ts +8 -0
- package/dist/core/hooks/index.d.ts.map +1 -0
- package/dist/core/hooks/index.js +7 -0
- package/dist/core/hooks/resolve/anchor.d.ts +37 -0
- package/dist/core/hooks/resolve/anchor.d.ts.map +1 -0
- package/dist/core/hooks/resolve/anchor.js +48 -0
- package/dist/core/hooks/resolve/coord-root.d.ts +6 -0
- package/dist/core/hooks/resolve/coord-root.d.ts.map +1 -0
- package/dist/core/hooks/resolve/coord-root.js +27 -0
- package/dist/core/hooks/resolve/intent.d.ts +33 -0
- package/dist/core/hooks/resolve/intent.d.ts.map +1 -0
- package/dist/core/hooks/resolve/intent.js +79 -0
- package/dist/core/hooks/resolve/owner.d.ts +42 -0
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -0
- package/dist/core/hooks/resolve/owner.js +140 -0
- package/dist/core/hooks/resolve/transcript.d.ts +26 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -0
- package/dist/core/hooks/resolve/transcript.js +73 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/lib/agent-browser/client.d.ts +99 -0
- package/dist/lib/agent-browser/client.d.ts.map +1 -0
- package/dist/lib/agent-browser/client.js +177 -0
- package/dist/lib/agent-browser/index.d.ts +2 -0
- package/dist/lib/agent-browser/index.d.ts.map +1 -0
- package/dist/lib/agent-browser/index.js +1 -0
- package/dist/lib/browser/client.d.ts +193 -0
- package/dist/lib/browser/client.d.ts.map +1 -0
- package/dist/lib/browser/client.js +325 -0
- package/dist/lib/browser/dev-overlay.d.ts +23 -0
- package/dist/lib/browser/dev-overlay.d.ts.map +1 -0
- package/dist/lib/browser/dev-overlay.js +153 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.d.ts.map +1 -0
- package/dist/lib/browser/index.js +2 -0
- package/dist/lib/browser/layout.d.ts +79 -0
- package/dist/lib/browser/layout.d.ts.map +1 -0
- package/dist/lib/browser/layout.js +220 -0
- package/dist/lib/browser/visibility.d.ts +86 -0
- package/dist/lib/browser/visibility.d.ts.map +1 -0
- package/dist/lib/browser/visibility.js +333 -0
- package/dist/lib/browser/visual-diff.d.ts +38 -0
- package/dist/lib/browser/visual-diff.d.ts.map +1 -0
- package/dist/lib/browser/visual-diff.js +107 -0
- package/dist/lib/completion/bash.d.ts +25 -0
- package/dist/lib/completion/bash.d.ts.map +1 -0
- package/dist/lib/completion/bash.js +284 -0
- package/dist/lib/completion/fish.d.ts +16 -0
- package/dist/lib/completion/fish.d.ts.map +1 -0
- package/dist/lib/completion/fish.js +118 -0
- package/dist/lib/completion/index.d.ts +5 -0
- package/dist/lib/completion/index.d.ts.map +1 -0
- package/dist/lib/completion/index.js +4 -0
- package/dist/lib/completion/walk.d.ts +68 -0
- package/dist/lib/completion/walk.d.ts.map +1 -0
- package/dist/lib/completion/walk.js +102 -0
- package/dist/lib/completion/zsh.d.ts +13 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -0
- package/dist/lib/completion/zsh.js +249 -0
- package/dist/lib/context/index.d.ts +107 -0
- package/dist/lib/context/index.d.ts.map +1 -0
- package/dist/lib/context/index.js +275 -0
- package/dist/lib/cookies/client.d.ts +131 -0
- package/dist/lib/cookies/client.d.ts.map +1 -0
- package/dist/lib/cookies/client.js +239 -0
- package/dist/lib/cookies/index.d.ts +2 -0
- package/dist/lib/cookies/index.d.ts.map +1 -0
- package/dist/lib/cookies/index.js +1 -0
- package/dist/lib/council/index.d.ts +266 -0
- package/dist/lib/council/index.d.ts.map +1 -0
- package/dist/lib/council/index.js +674 -0
- package/dist/lib/docs-index.d.ts +28 -0
- package/dist/lib/docs-index.d.ts.map +1 -0
- package/dist/lib/docs-index.js +169 -0
- package/dist/lib/docs-lint.d.ts +26 -0
- package/dist/lib/docs-lint.d.ts.map +1 -0
- package/dist/lib/docs-lint.js +378 -0
- package/dist/lib/docs-sweep.d.ts +34 -0
- package/dist/lib/docs-sweep.d.ts.map +1 -0
- package/dist/lib/docs-sweep.js +304 -0
- package/dist/lib/docs.d.ts +27 -0
- package/dist/lib/docs.d.ts.map +1 -0
- package/dist/lib/docs.js +142 -0
- package/dist/lib/env.d.ts +11 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +12 -0
- package/dist/lib/exec.d.ts +32 -0
- package/dist/lib/exec.d.ts.map +1 -0
- package/dist/lib/exec.js +54 -0
- package/dist/lib/format.d.ts +29 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/lib/format.js +139 -0
- package/dist/lib/http/client.d.ts +56 -0
- package/dist/lib/http/client.d.ts.map +1 -0
- package/dist/lib/http/client.js +160 -0
- package/dist/lib/http/index.d.ts +2 -0
- package/dist/lib/http/index.d.ts.map +1 -0
- package/dist/lib/http/index.js +1 -0
- package/dist/lib/identities/index.d.ts +77 -0
- package/dist/lib/identities/index.d.ts.map +1 -0
- package/dist/lib/identities/index.js +190 -0
- package/dist/lib/machine.d.ts +19 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/machine.js +61 -0
- package/dist/lib/presence.d.ts +48 -0
- package/dist/lib/presence.d.ts.map +1 -0
- package/dist/lib/presence.js +123 -0
- package/dist/lib/readability/client.d.ts +32 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +119 -0
- package/dist/lib/readability/index.d.ts +2 -0
- package/dist/lib/readability/index.d.ts.map +1 -0
- package/dist/lib/readability/index.js +1 -0
- package/dist/lib/scratch/index.d.ts +74 -0
- package/dist/lib/scratch/index.d.ts.map +1 -0
- package/dist/lib/scratch/index.js +393 -0
- package/dist/lib/tunnel/gate.d.ts +12 -0
- package/dist/lib/tunnel/gate.d.ts.map +1 -0
- package/dist/lib/tunnel/gate.js +101 -0
- package/dist/lib/tunnel/state.d.ts +34 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -0
- package/dist/lib/tunnel/state.js +132 -0
- package/package.json +160 -8
- package/schemas/.gitkeep +0 -0
- package/schemas/config.schema.json +109 -0
- package/src/cli.ts +22 -0
- package/src/commander.ts +242 -0
- package/src/commands/.gitkeep +0 -0
- package/src/commands/agents.ts +4567 -0
- package/src/commands/backup.ts +305 -0
- package/src/commands/browse-ai.ts +198 -0
- package/src/commands/browse.ts +849 -0
- package/src/commands/callers.ts +363 -0
- package/src/commands/completion.ts +193 -0
- package/src/commands/config-get.ts +161 -0
- package/src/commands/context.ts +209 -0
- package/src/commands/cookies.ts +198 -0
- package/src/commands/docs.ts +174 -0
- package/src/commands/doctor.ts +231 -0
- package/src/commands/edit-batch.ts +233 -0
- package/src/commands/eml.ts +519 -0
- package/src/commands/env.ts +254 -0
- package/src/commands/fetch.ts +136 -0
- package/src/commands/file-history.ts +202 -0
- package/src/commands/grep.ts +371 -0
- package/src/commands/init.ts +335 -0
- package/src/commands/outline.ts +564 -0
- package/src/commands/presence.ts +152 -0
- package/src/commands/read.ts +64 -0
- package/src/commands/scratch.ts +445 -0
- package/src/commands/session.ts +187 -0
- package/src/commands/sync.ts +306 -0
- package/src/commands/toc.ts +218 -0
- package/src/commands/tokens.ts +79 -0
- package/src/commands/tunnel.ts +633 -0
- package/src/commands/uninstall.ts +144 -0
- package/src/commands/web.ts +193 -0
- package/src/core/agents/canonical-emit.ts +77 -0
- package/src/core/agents/cli-emit.ts +64 -0
- package/src/core/agents/cli.ts +838 -0
- package/src/core/agents/codex-replay.ts +163 -0
- package/src/core/agents/coord-client.ts +249 -0
- package/src/core/agents/events/consume.ts +196 -0
- package/src/core/agents/events/emit.ts +108 -0
- package/src/core/agents/events/ulid.ts +51 -0
- package/src/core/agents/index.ts +14 -0
- package/src/core/agents/paths.ts +16 -0
- package/src/core/agents/render/prompt-context.ts +401 -0
- package/src/core/agents/render/session-context.ts +341 -0
- package/src/core/agents/rules/claim-conflict.ts +282 -0
- package/src/core/agents/rules/commit-conflict.ts +303 -0
- package/src/core/agents/rules/stop-hook.ts +229 -0
- package/src/core/agents/session-events.ts +228 -0
- package/src/core/agents/state/activity-log.ts +33 -0
- package/src/core/agents/state/council.ts +265 -0
- package/src/core/agents/state/heartbeat-projector.ts +488 -0
- package/src/core/agents/state/heartbeat-writer.ts +333 -0
- package/src/core/agents/state/names.ts +399 -0
- package/src/core/agents/state/pidmap.ts +38 -0
- package/src/core/agents/state/scratch.ts +121 -0
- package/src/core/agents/state/shell-mutation.ts +44 -0
- package/src/core/agents/state/stale-sweep.ts +190 -0
- package/src/core/config.ts +111 -0
- package/src/core/hooks/cli.ts +1247 -0
- package/src/core/hooks/effects/image-capture.ts +330 -0
- package/src/core/hooks/effects/index.ts +210 -0
- package/src/core/hooks/events/emit.ts +120 -0
- package/src/core/hooks/events/schema.ts +430 -0
- package/src/core/hooks/events/ulid.ts +51 -0
- package/src/core/hooks/harness/detect.ts +30 -0
- package/src/core/hooks/harness/events.ts +102 -0
- package/src/core/hooks/harness/output.ts +100 -0
- package/src/core/hooks/harness/parse.ts +180 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/resolve/anchor.ts +51 -0
- package/src/core/hooks/resolve/coord-root.ts +25 -0
- package/src/core/hooks/resolve/intent.ts +89 -0
- package/src/core/hooks/resolve/owner.ts +140 -0
- package/src/core/hooks/resolve/transcript.ts +72 -0
- package/src/hooks/.gitkeep +0 -0
- package/src/index.ts +15 -0
- package/src/lib/agent-browser/client.ts +239 -0
- package/src/lib/agent-browser/index.ts +1 -0
- package/src/lib/browser/client.ts +449 -0
- package/src/lib/browser/dev-overlay.ts +207 -0
- package/src/lib/browser/index.ts +24 -0
- package/src/lib/browser/layout.ts +288 -0
- package/src/lib/browser/visibility.ts +419 -0
- package/src/lib/browser/visual-diff.ts +150 -0
- package/src/lib/completion/bash.ts +291 -0
- package/src/lib/completion/fish.ts +134 -0
- package/src/lib/completion/index.ts +10 -0
- package/src/lib/completion/walk.ts +184 -0
- package/src/lib/completion/zsh.ts +262 -0
- package/src/lib/context/index.ts +386 -0
- package/src/lib/cookies/client.ts +301 -0
- package/src/lib/cookies/index.ts +13 -0
- package/src/lib/council/index.ts +803 -0
- package/src/lib/docs-index.ts +216 -0
- package/src/lib/docs-lint.ts +413 -0
- package/src/lib/docs-sweep.ts +348 -0
- package/src/lib/docs.ts +199 -0
- package/src/lib/env.ts +12 -0
- package/src/lib/exec.ts +74 -0
- package/src/lib/format.ts +147 -0
- package/src/lib/http/client.ts +211 -0
- package/src/lib/http/index.ts +1 -0
- package/src/lib/identities/index.ts +210 -0
- package/src/lib/machine.ts +61 -0
- package/src/lib/presence.ts +154 -0
- package/src/lib/readability/client.ts +156 -0
- package/src/lib/readability/index.ts +5 -0
- package/src/lib/readability/turndown-plugin-gfm.d.ts +10 -0
- package/src/lib/scratch/index.ts +470 -0
- package/src/lib/tunnel/gate.ts +113 -0
- package/src/lib/tunnel/state.ts +167 -0
- package/src/web/.gitkeep +0 -0
- package/index.js +0 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
5
|
+
import { exec, sh } from "../lib/exec.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `env`: show environment status across runtimes, docker, gcp, bq, git.
|
|
9
|
+
*
|
|
10
|
+
* Monorepo state (repoRoot, submodules) flows in via HarneryProgramContext.
|
|
11
|
+
* When neither is provided (harn invoked outside a monorepo), the git section
|
|
12
|
+
* reports just the current branch and skips the submodule row.
|
|
13
|
+
*
|
|
14
|
+
* Color hints are intentionally string labels (`"ok"`/`"missing"`/...) rather
|
|
15
|
+
* than ANSI-wrapper functions, so consumer adapters can consume them as
|
|
16
|
+
* metadata; defaultEmit just JSON-stringifies.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface Check {
|
|
20
|
+
label: string;
|
|
21
|
+
value: string;
|
|
22
|
+
status?: "ok" | "missing" | "warn" | "info";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function registerEnvCommand(
|
|
26
|
+
program: Command,
|
|
27
|
+
emit: EmitContext,
|
|
28
|
+
context?: HarneryProgramContext,
|
|
29
|
+
): void {
|
|
30
|
+
program
|
|
31
|
+
.command("env [section]")
|
|
32
|
+
.description("Show environment status (docker, gcp, bq, node, python, git)")
|
|
33
|
+
.action(async (section?: string) => {
|
|
34
|
+
try {
|
|
35
|
+
await handleEnv(section, emit, context);
|
|
36
|
+
} catch (err: unknown) {
|
|
37
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38
|
+
emit.error({ code: "env_error", message: msg });
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function handleEnv(
|
|
45
|
+
section: string | undefined,
|
|
46
|
+
emit: EmitContext,
|
|
47
|
+
context: HarneryProgramContext | undefined,
|
|
48
|
+
): Promise<void> {
|
|
49
|
+
const sections: Record<string, () => Promise<Check[]>> = {
|
|
50
|
+
runtimes: checkRuntimes,
|
|
51
|
+
docker: checkDocker,
|
|
52
|
+
gcp: checkGcp,
|
|
53
|
+
bq: checkBigQuery,
|
|
54
|
+
git: () => checkGit(context),
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (section) {
|
|
58
|
+
const fn = sections[section];
|
|
59
|
+
if (!fn) {
|
|
60
|
+
emit.error({
|
|
61
|
+
code: "unknown_section",
|
|
62
|
+
message: `Unknown section "${section}". Valid: ${Object.keys(sections).join(", ")}`,
|
|
63
|
+
});
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
const checks = await fn();
|
|
67
|
+
emit.data({ ok: true, section, checks: checks.map(toRow) });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const results = await Promise.all(
|
|
72
|
+
Object.entries(sections).map(async ([name, fn]) => ({
|
|
73
|
+
name,
|
|
74
|
+
checks: await fn().catch(() => [
|
|
75
|
+
{ label: name, value: "check failed", status: "missing" as const },
|
|
76
|
+
]),
|
|
77
|
+
})),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
emit.data({
|
|
81
|
+
ok: true,
|
|
82
|
+
sections: Object.fromEntries(results.map(({ name, checks }) => [name, checks.map(toRow)])),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toRow(check: Check): { label: string; value: string } {
|
|
87
|
+
return { label: check.label, value: check.value };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function checkRuntimes(): Promise<Check[]> {
|
|
91
|
+
const checks: Check[] = [];
|
|
92
|
+
|
|
93
|
+
const [node, bun, python, php] = await Promise.all([
|
|
94
|
+
exec(["node", "--version"]).catch(() => ({ stdout: "", exitCode: 1, stderr: "" })),
|
|
95
|
+
exec(["bun", "--version"]).catch(() => ({ stdout: "", exitCode: 1, stderr: "" })),
|
|
96
|
+
exec(["python3", "--version"]).catch(() => ({ stdout: "", exitCode: 1, stderr: "" })),
|
|
97
|
+
exec(["php", "--version"]).catch(() => ({ stdout: "", exitCode: 1, stderr: "" })),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
if (node.exitCode === 0) checks.push({ label: "Node.js", value: node.stdout, status: "ok" });
|
|
101
|
+
else checks.push({ label: "Node.js", value: "not found", status: "missing" });
|
|
102
|
+
|
|
103
|
+
if (bun.exitCode === 0) checks.push({ label: "Bun", value: bun.stdout, status: "ok" });
|
|
104
|
+
else checks.push({ label: "Bun", value: "not found", status: "missing" });
|
|
105
|
+
|
|
106
|
+
if (python.exitCode === 0)
|
|
107
|
+
checks.push({ label: "Python", value: python.stdout.replace("Python ", ""), status: "ok" });
|
|
108
|
+
else checks.push({ label: "Python", value: "not found", status: "missing" });
|
|
109
|
+
|
|
110
|
+
if (php.exitCode === 0) {
|
|
111
|
+
const ver = php.stdout.split("\n")[0]?.match(/PHP (\S+)/)?.[1] ?? php.stdout;
|
|
112
|
+
checks.push({ label: "PHP", value: ver, status: "ok" });
|
|
113
|
+
} else {
|
|
114
|
+
checks.push({ label: "PHP", value: "not found", status: "info" });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return checks;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function checkDocker(): Promise<Check[]> {
|
|
121
|
+
const checks: Check[] = [];
|
|
122
|
+
|
|
123
|
+
const dockerVersion = await exec(["docker", "--version"]).catch(() => ({
|
|
124
|
+
stdout: "",
|
|
125
|
+
exitCode: 1,
|
|
126
|
+
stderr: "",
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
if (dockerVersion.exitCode !== 0) {
|
|
130
|
+
return [{ label: "Docker", value: "not installed", status: "missing" }];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const ver = dockerVersion.stdout.match(/Docker version (\S+)/)?.[1] ?? dockerVersion.stdout;
|
|
134
|
+
checks.push({ label: "Docker", value: ver, status: "ok" });
|
|
135
|
+
|
|
136
|
+
const ps = await sh('docker ps --format "{{.Names}}" 2>/dev/null').catch(() => ({
|
|
137
|
+
stdout: "",
|
|
138
|
+
exitCode: 1,
|
|
139
|
+
stderr: "",
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
if (ps.exitCode === 0) {
|
|
143
|
+
const names = ps.stdout.split("\n").filter(Boolean);
|
|
144
|
+
checks.push({
|
|
145
|
+
label: "Containers",
|
|
146
|
+
value: names.length > 0 ? `${names.length} running` : "none running",
|
|
147
|
+
status: names.length > 0 ? "ok" : "info",
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
checks.push({ label: "Containers", value: "docker ps unavailable", status: "info" });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return checks;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function checkGcp(): Promise<Check[]> {
|
|
157
|
+
const checks: Check[] = [];
|
|
158
|
+
|
|
159
|
+
const account = await sh("gcloud config get-value account 2>/dev/null").catch(() => ({
|
|
160
|
+
stdout: "",
|
|
161
|
+
exitCode: 1,
|
|
162
|
+
stderr: "",
|
|
163
|
+
}));
|
|
164
|
+
const project = await sh("gcloud config get-value project 2>/dev/null").catch(() => ({
|
|
165
|
+
stdout: "",
|
|
166
|
+
exitCode: 1,
|
|
167
|
+
stderr: "",
|
|
168
|
+
}));
|
|
169
|
+
|
|
170
|
+
if (account.exitCode === 0 && account.stdout) {
|
|
171
|
+
checks.push({ label: "GCP Account", value: account.stdout, status: "ok" });
|
|
172
|
+
} else {
|
|
173
|
+
checks.push({ label: "GCP Account", value: "not authenticated", status: "missing" });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (project.exitCode === 0 && project.stdout) {
|
|
177
|
+
checks.push({ label: "GCP Project", value: project.stdout, status: "ok" });
|
|
178
|
+
} else {
|
|
179
|
+
checks.push({ label: "GCP Project", value: "not set", status: "missing" });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return checks;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function checkBigQuery(): Promise<Check[]> {
|
|
186
|
+
const checks: Check[] = [];
|
|
187
|
+
|
|
188
|
+
const result = await sh("bq ls --max_results=1 2>/dev/null").catch(() => ({
|
|
189
|
+
stdout: "",
|
|
190
|
+
exitCode: 1,
|
|
191
|
+
stderr: "",
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
if (result.exitCode === 0) {
|
|
195
|
+
checks.push({ label: "BigQuery", value: "connected", status: "ok" });
|
|
196
|
+
} else {
|
|
197
|
+
checks.push({ label: "BigQuery", value: "not connected (check GCP auth)", status: "missing" });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const datasets = await sh("bq ls --format=json --max_results=20 2>/dev/null").catch(() => ({
|
|
201
|
+
stdout: "",
|
|
202
|
+
exitCode: 1,
|
|
203
|
+
stderr: "",
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
if (datasets.exitCode === 0 && datasets.stdout) {
|
|
207
|
+
try {
|
|
208
|
+
const ds = JSON.parse(datasets.stdout) as { datasetReference?: { datasetId?: string } }[];
|
|
209
|
+
const names = ds
|
|
210
|
+
.map((d) => d.datasetReference?.datasetId)
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join(", ");
|
|
213
|
+
if (names) checks.push({ label: "Datasets", value: names, status: "info" });
|
|
214
|
+
} catch {
|
|
215
|
+
// Ignore parse failures
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return checks;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function checkGit(context: HarneryProgramContext | undefined): Promise<Check[]> {
|
|
223
|
+
const checks: Check[] = [];
|
|
224
|
+
const cwd = context?.repoRoot ?? process.cwd();
|
|
225
|
+
|
|
226
|
+
const branch = await exec(["git", "rev-parse", "--abbrev-ref", "HEAD"], { cwd });
|
|
227
|
+
checks.push({ label: "Branch", value: branch.stdout, status: "info" });
|
|
228
|
+
|
|
229
|
+
if (context?.repoRoot && context.submodules && context.submodules.length > 0) {
|
|
230
|
+
const initialized = context.submodules.filter((name) =>
|
|
231
|
+
isSubmoduleInitialized(resolve(context.repoRoot as string, name)),
|
|
232
|
+
);
|
|
233
|
+
const total = context.submodules.length;
|
|
234
|
+
checks.push({
|
|
235
|
+
label: "Submodules",
|
|
236
|
+
value: `${initialized.length}/${total} initialized`,
|
|
237
|
+
status: initialized.length === total ? "ok" : "warn",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return checks;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function isSubmoduleInitialized(dir: string): boolean {
|
|
245
|
+
if (!existsSync(dir)) return false;
|
|
246
|
+
try {
|
|
247
|
+
// readdirSync lists every entry (dotfiles included, `.`/`..` excluded):
|
|
248
|
+
// the non-recursive "is this dir non-empty?" check this needs, on both
|
|
249
|
+
// Bun and Node (replaces the Bun-only `new Bun.Glob("*").scanSync`).
|
|
250
|
+
return readdirSync(dir).length > 0;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import type { Command } from "commander";
|
|
5
|
+
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
6
|
+
import { CookieJar } from "../lib/cookies/index.ts";
|
|
7
|
+
import { fetchWithJar } from "../lib/http/index.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `harn fetch`: HTTP request with cookie-jar attach + persist.
|
|
11
|
+
*
|
|
12
|
+
* Default jar is the same one `harn cookies` reads/writes
|
|
13
|
+
* (`~/.cache/harnery/cookies.json`), so cookies set during a `harn browse`
|
|
14
|
+
* session flow naturally to subsequent `harn fetch` calls.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const DEFAULT_STORE = resolve(homedir(), ".cache", "harnery", "cookies.json");
|
|
18
|
+
|
|
19
|
+
interface FetchOpts {
|
|
20
|
+
method: string;
|
|
21
|
+
header?: string[];
|
|
22
|
+
data?: string;
|
|
23
|
+
output?: string;
|
|
24
|
+
store?: string;
|
|
25
|
+
cookies?: boolean;
|
|
26
|
+
redirect: string;
|
|
27
|
+
timeout: string;
|
|
28
|
+
status?: boolean;
|
|
29
|
+
headers?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function registerFetchCommand(
|
|
34
|
+
program: Command,
|
|
35
|
+
emit: EmitContext,
|
|
36
|
+
context?: HarneryProgramContext,
|
|
37
|
+
): void {
|
|
38
|
+
program
|
|
39
|
+
.command("fetch <url>")
|
|
40
|
+
.description(
|
|
41
|
+
"HTTP GET (or --method) with cookie-jar attach + persist. " +
|
|
42
|
+
"Default jar is ~/.cache/harnery/cookies.json (shared with harn cookies / harn browse).",
|
|
43
|
+
)
|
|
44
|
+
.option("-X, --method <method>", "HTTP method", "GET")
|
|
45
|
+
.option("-H, --header <header...>", "Extra request header (repeatable, format: 'Name: value')")
|
|
46
|
+
.option("-d, --data <body>", "Request body")
|
|
47
|
+
.option("-o, --output <file>", "Write response body to file (default: stdout)")
|
|
48
|
+
.option("--store <path>", `Cookie store path (default ${DEFAULT_STORE})`)
|
|
49
|
+
.option("--no-cookies", "Skip cookie-jar attach + persist")
|
|
50
|
+
.option("--redirect <mode>", "Redirect handling: follow | error | manual", "follow")
|
|
51
|
+
.option("--timeout <ms>", "Request timeout in milliseconds", "30000")
|
|
52
|
+
.option("--status", "Print status to stderr")
|
|
53
|
+
.option("--headers", "Print response headers to stderr")
|
|
54
|
+
.option("--json", "Output the full FetchResult (status, headers, body) as JSON")
|
|
55
|
+
.action(async (url: string, opts: FetchOpts) => {
|
|
56
|
+
try {
|
|
57
|
+
await runFetch(url, opts, emit, context);
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
emit.error({ code: "fetch_error", message: msg });
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function runFetch(
|
|
67
|
+
url: string,
|
|
68
|
+
opts: FetchOpts,
|
|
69
|
+
emit: EmitContext,
|
|
70
|
+
context: HarneryProgramContext | undefined,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const headers: Record<string, string> = {};
|
|
73
|
+
for (const h of opts.header ?? []) {
|
|
74
|
+
const idx = h.indexOf(":");
|
|
75
|
+
if (idx < 0) {
|
|
76
|
+
throw new Error(`Bad header (expected 'Name: value'): ${h}`);
|
|
77
|
+
}
|
|
78
|
+
headers[h.slice(0, idx).trim()] = h.slice(idx + 1).trim();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const jar =
|
|
82
|
+
opts.cookies !== false
|
|
83
|
+
? new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "bp-fetch" })
|
|
84
|
+
: null;
|
|
85
|
+
|
|
86
|
+
const timeoutMs = Number.parseInt(opts.timeout, 10);
|
|
87
|
+
const ac = new AbortController();
|
|
88
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
89
|
+
let result: Awaited<ReturnType<typeof fetchWithJar>>;
|
|
90
|
+
try {
|
|
91
|
+
result = await fetchWithJar(url, {
|
|
92
|
+
method: opts.method,
|
|
93
|
+
headers,
|
|
94
|
+
body: opts.data,
|
|
95
|
+
jar,
|
|
96
|
+
redirect: opts.redirect as RequestRedirect,
|
|
97
|
+
signal: ac.signal,
|
|
98
|
+
extraHeaders: context?.extraHeaders,
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (opts.json) {
|
|
105
|
+
emit.data(result as unknown as Record<string, unknown>);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (opts.status) {
|
|
109
|
+
emit.log(`${result.status} ${result.statusText} ${result.url}`, "info");
|
|
110
|
+
}
|
|
111
|
+
if (opts.headers) {
|
|
112
|
+
for (const [k, v] of Object.entries(result.headers)) {
|
|
113
|
+
emit.log(`${k}: ${v}`, "info");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (opts.output) {
|
|
118
|
+
writeFileSync(opts.output, result.body);
|
|
119
|
+
emit.file(opts.output, {
|
|
120
|
+
bytes: result.body.length,
|
|
121
|
+
status: result.status,
|
|
122
|
+
status_text: result.statusText,
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
// Body is potentially binary; route as text since most fetch responses
|
|
126
|
+
// are text/HTML/JSON. Binary callers should use --output.
|
|
127
|
+
emit.text(result.body);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (jar && result.cookiesSaved > 0) {
|
|
131
|
+
emit.log(
|
|
132
|
+
`saved ${result.cookiesSaved} cookie${result.cookiesSaved === 1 ? "" : "s"} to ${jar.path}`,
|
|
133
|
+
"info",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
|
+
import type { Command } from "commander";
|
|
4
|
+
import type { EmitContext } from "../commander.ts";
|
|
5
|
+
import { exec } from "../lib/exec.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `file-history <path>`: concise per-file git history. Total commits, distinct
|
|
9
|
+
* authors, first/last touched dates, total line impact, and the N most-recent
|
|
10
|
+
* commits with +/- counts. Follows renames. Submodule-aware via git itself:
|
|
11
|
+
* runs the log inside whichever repo `git rev-parse --show-toplevel` resolves
|
|
12
|
+
* from the file's directory, so submodule files get the submodule's history.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
interface FileHistoryOpts {
|
|
16
|
+
limit?: string;
|
|
17
|
+
since?: string;
|
|
18
|
+
author?: string;
|
|
19
|
+
json?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CommitEntry {
|
|
23
|
+
sha: string;
|
|
24
|
+
short_sha: string;
|
|
25
|
+
author: string;
|
|
26
|
+
date: string;
|
|
27
|
+
subject: string;
|
|
28
|
+
added: number;
|
|
29
|
+
removed: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FileHistoryResult {
|
|
33
|
+
file: string;
|
|
34
|
+
repo: string;
|
|
35
|
+
total_commits: number;
|
|
36
|
+
authors: { name: string; commits: number }[];
|
|
37
|
+
first_commit: { sha: string; date: string } | null;
|
|
38
|
+
last_commit: { sha: string; date: string } | null;
|
|
39
|
+
total_added: number;
|
|
40
|
+
total_removed: number;
|
|
41
|
+
commits: CommitEntry[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function registerFileHistoryCommand(program: Command, emit: EmitContext): void {
|
|
45
|
+
program
|
|
46
|
+
.command("file-history <path>")
|
|
47
|
+
.description(
|
|
48
|
+
"Concise per-file git history: summary stats + N most-recent commits with line impact.",
|
|
49
|
+
)
|
|
50
|
+
.option("--limit <n>", "Show N most-recent commits", "20")
|
|
51
|
+
.option("--since <date>", "Restrict to commits since DATE (e.g. '30 days ago', '2026-01-01')")
|
|
52
|
+
.option("--author <pat>", "Filter to commits by author (substring match)")
|
|
53
|
+
.option("--json", "Structured JSON envelope")
|
|
54
|
+
.action(async (path: string, opts: FileHistoryOpts) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await runFileHistory(path, opts);
|
|
57
|
+
if (opts.json) {
|
|
58
|
+
emit.config({ format: "json" });
|
|
59
|
+
emit.data(result);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
emit.text(`${renderFileHistory(result)}\n`);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
emit.error({ code: "file_history_failed", message: (err as Error).message });
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function runFileHistory(path: string, opts: FileHistoryOpts): Promise<FileHistoryResult> {
|
|
71
|
+
const absPath = resolve(path);
|
|
72
|
+
if (!existsSync(absPath)) throw new Error(`no such file: ${path}`);
|
|
73
|
+
|
|
74
|
+
const repo = await detectRepo(absPath);
|
|
75
|
+
const relPath = relative(repo.cwd, absPath);
|
|
76
|
+
if (relPath.startsWith("..")) {
|
|
77
|
+
throw new Error(`path outside detected repo (${repo.cwd}): ${absPath}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const args = [
|
|
81
|
+
"log",
|
|
82
|
+
"--no-merges",
|
|
83
|
+
"--follow",
|
|
84
|
+
"--pretty=format:%H%x09%an%x09%aI%x09%s",
|
|
85
|
+
"--shortstat",
|
|
86
|
+
];
|
|
87
|
+
if (opts.since) args.push(`--since=${opts.since}`);
|
|
88
|
+
if (opts.author) args.push(`--author=${opts.author}`);
|
|
89
|
+
args.push("--", relPath);
|
|
90
|
+
|
|
91
|
+
const log = await exec(["git", ...args], { cwd: repo.cwd, timeout: 30_000 });
|
|
92
|
+
if (log.exitCode !== 0) throw new Error(`git log failed: ${log.stderr || log.stdout}`);
|
|
93
|
+
|
|
94
|
+
const commits = parseGitLog(log.stdout);
|
|
95
|
+
|
|
96
|
+
const authorMap = new Map<string, number>();
|
|
97
|
+
let totalAdded = 0;
|
|
98
|
+
let totalRemoved = 0;
|
|
99
|
+
for (const c of commits) {
|
|
100
|
+
authorMap.set(c.author, (authorMap.get(c.author) || 0) + 1);
|
|
101
|
+
totalAdded += c.added;
|
|
102
|
+
totalRemoved += c.removed;
|
|
103
|
+
}
|
|
104
|
+
const authors = Array.from(authorMap.entries())
|
|
105
|
+
.map(([name, commits]) => ({ name, commits }))
|
|
106
|
+
.sort((a, b) => b.commits - a.commits);
|
|
107
|
+
|
|
108
|
+
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : 20;
|
|
109
|
+
const recentCommits = commits.slice(0, Number.isFinite(limit) ? limit : 20);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
file: relPath,
|
|
113
|
+
repo: repo.name,
|
|
114
|
+
total_commits: commits.length,
|
|
115
|
+
authors,
|
|
116
|
+
first_commit:
|
|
117
|
+
commits.length > 0
|
|
118
|
+
? { sha: commits[commits.length - 1]!.short_sha, date: commits[commits.length - 1]!.date }
|
|
119
|
+
: null,
|
|
120
|
+
last_commit: commits.length > 0 ? { sha: commits[0]!.short_sha, date: commits[0]!.date } : null,
|
|
121
|
+
total_added: totalAdded,
|
|
122
|
+
total_removed: totalRemoved,
|
|
123
|
+
commits: recentCommits,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseGitLog(output: string): CommitEntry[] {
|
|
128
|
+
const commits: CommitEntry[] = [];
|
|
129
|
+
const lines = output.split("\n");
|
|
130
|
+
let current: CommitEntry | null = null;
|
|
131
|
+
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
if (line.includes("\t")) {
|
|
134
|
+
if (current) commits.push(current);
|
|
135
|
+
const [sha, author, date, ...rest] = line.split("\t");
|
|
136
|
+
current = {
|
|
137
|
+
sha: sha!,
|
|
138
|
+
short_sha: sha!.slice(0, 8),
|
|
139
|
+
author: author!,
|
|
140
|
+
date: date!,
|
|
141
|
+
subject: rest.join("\t"),
|
|
142
|
+
added: 0,
|
|
143
|
+
removed: 0,
|
|
144
|
+
};
|
|
145
|
+
} else if (current && /\bfile[s]?\b.*changed/.test(line)) {
|
|
146
|
+
const ins = line.match(/(\d+) insertion/);
|
|
147
|
+
const del = line.match(/(\d+) deletion/);
|
|
148
|
+
if (ins) current.added = Number.parseInt(ins[1]!, 10);
|
|
149
|
+
if (del) current.removed = Number.parseInt(del[1]!, 10);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (current) commits.push(current);
|
|
153
|
+
|
|
154
|
+
return commits;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function detectRepo(absPath: string): Promise<{ name: string; cwd: string }> {
|
|
158
|
+
const dir = dirname(absPath);
|
|
159
|
+
const result = await exec(["git", "rev-parse", "--show-toplevel"], { cwd: dir });
|
|
160
|
+
if (result.exitCode !== 0) {
|
|
161
|
+
throw new Error(`not inside a git repository: ${absPath}`);
|
|
162
|
+
}
|
|
163
|
+
const cwd = result.stdout.trim();
|
|
164
|
+
return { name: basename(cwd), cwd };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderFileHistory(r: FileHistoryResult): string {
|
|
168
|
+
const lines: string[] = [];
|
|
169
|
+
lines.push(`file-history · ${r.repo}/${r.file}`);
|
|
170
|
+
lines.push("");
|
|
171
|
+
|
|
172
|
+
if (r.total_commits === 0) {
|
|
173
|
+
lines.push("(no commits found for this path)");
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lines.push(
|
|
178
|
+
`summary: ${r.total_commits} commit${r.total_commits === 1 ? "" : "s"} by ${r.authors.length} author${r.authors.length === 1 ? "" : "s"}, +${r.total_added}/-${r.total_removed} lines`,
|
|
179
|
+
);
|
|
180
|
+
if (r.last_commit) lines.push(` last: ${r.last_commit.sha} ${r.last_commit.date}`);
|
|
181
|
+
if (r.first_commit) lines.push(` first: ${r.first_commit.sha} ${r.first_commit.date}`);
|
|
182
|
+
|
|
183
|
+
if (r.authors.length > 0) {
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push("authors:");
|
|
186
|
+
for (const a of r.authors.slice(0, 5)) {
|
|
187
|
+
lines.push(` ${a.commits.toString().padStart(3)} ${a.name}`);
|
|
188
|
+
}
|
|
189
|
+
if (r.authors.length > 5) lines.push(` ... +${r.authors.length - 5} more`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
lines.push("");
|
|
193
|
+
lines.push(`recent (showing ${r.commits.length} of ${r.total_commits}):`);
|
|
194
|
+
for (const c of r.commits) {
|
|
195
|
+
const dateOnly = c.date.slice(0, 10);
|
|
196
|
+
const sig = `+${c.added}/-${c.removed}`.padStart(10);
|
|
197
|
+
const author = c.author.slice(0, 18).padEnd(18);
|
|
198
|
+
lines.push(` ${c.short_sha} ${dateOnly} ${sig} ${author} ${c.subject}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return lines.join("\n");
|
|
202
|
+
}
|