harnery 0.0.1 → 0.2.1
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 +509 -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 +39 -0
- package/dist/lib/readability/client.d.ts.map +1 -0
- package/dist/lib/readability/client.js +121 -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 +583 -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 +169 -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,513 @@
|
|
|
1
|
+
// Inline cachePath; see lib/tunnel/state.ts for rationale.
|
|
2
|
+
function cachePath(tool, filename) {
|
|
3
|
+
const dir = resolve(process.cwd(), ".cache", tool);
|
|
4
|
+
return resolve(dir, filename);
|
|
5
|
+
}
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
import { cfdLogFile, clearState, DEFAULT_INSTANCE, ensureCloudflared, gateLogFile, isProcessAlive, listStates, readConfig, readState, writeConfig, writeState, } from "../lib/tunnel/state.js";
|
|
10
|
+
/**
|
|
11
|
+
* `tunnel`: IP-gated Cloudflare quick tunnel in front of a local upstream.
|
|
12
|
+
*
|
|
13
|
+
* Two-process design: a Bun reverse-proxy worker (lib/tunnel/gate.ts) checks
|
|
14
|
+
* the Cloudflare-set `CF-Connecting-IP` header against an allowlist and
|
|
15
|
+
* rewrites Host for the upstream; cloudflared --url then exposes the gate
|
|
16
|
+
* as a random `<words>.trycloudflare.com` hostname.
|
|
17
|
+
*
|
|
18
|
+
* State + config persisted under `.cache/tunnel/`. cloudflared auto-installs
|
|
19
|
+
* to ~/.local/bin/ on first run (Linux only; macOS users `brew install`).
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_TARGET = "127.0.0.1:8001";
|
|
22
|
+
const DEFAULT_VHOST = "localhost";
|
|
23
|
+
const DEFAULT_GATE_PORT = 9001;
|
|
24
|
+
const MAX_GATE_PORT = DEFAULT_GATE_PORT + 99; // auto-allocation scan ceiling
|
|
25
|
+
/**
|
|
26
|
+
* Validate + normalize an instance name. Names become filename fragments
|
|
27
|
+
* (state-<name>.json) and pgrep patterns, so they're restricted to a safe
|
|
28
|
+
* charset. Throws a friendly emit.error + exits on a bad name.
|
|
29
|
+
*/
|
|
30
|
+
function resolveName(raw) {
|
|
31
|
+
const name = (raw ?? DEFAULT_INSTANCE).trim();
|
|
32
|
+
if (!/^[a-z0-9][a-z0-9-]*$/i.test(name)) {
|
|
33
|
+
emit.error({
|
|
34
|
+
code: "tunnel_bad_name",
|
|
35
|
+
message: `Invalid instance name "${name}". Use letters, digits, and dashes (must start alphanumeric).`,
|
|
36
|
+
});
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
function gateScriptPath() {
|
|
42
|
+
return resolve(import.meta.dirname, "..", "lib", "tunnel", "gate.ts");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `harn tunnel` is the one command that hard-requires Bun: the gate worker is a
|
|
46
|
+
* `Bun.serve` process (HTTP + WebSocket reverse proxy), spawned as `bun run
|
|
47
|
+
* gate.ts`. Everything else in harnery runs on Node, but this can't until the
|
|
48
|
+
* gate is ported off `Bun.serve` (node:http + `ws`). Detect Bun up front so the
|
|
49
|
+
* failure is a clear message rather than an opaque ENOENT from the gate spawn.
|
|
50
|
+
*/
|
|
51
|
+
function bunAvailable() {
|
|
52
|
+
return spawnSync("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
53
|
+
}
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Kill every process whose command line matches `pattern` (via `pgrep -f`),
|
|
59
|
+
* skipping our own PID and any already-killed. Returns the count killed. Used
|
|
60
|
+
* as a fallback so orphaned gate/cloudflared processes get cleaned even when
|
|
61
|
+
* the state file was lost (which otherwise left them squatting on the port).
|
|
62
|
+
*/
|
|
63
|
+
function killByPattern(pattern, alreadyKilled) {
|
|
64
|
+
const r = spawnSync("pgrep", ["-f", pattern], { encoding: "utf-8" });
|
|
65
|
+
if (r.status !== 0 || typeof r.stdout !== "string")
|
|
66
|
+
return 0;
|
|
67
|
+
let killed = 0;
|
|
68
|
+
for (const line of r.stdout.split("\n")) {
|
|
69
|
+
const pid = Number(line.trim());
|
|
70
|
+
if (!pid || pid === process.pid || alreadyKilled.has(pid))
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid);
|
|
74
|
+
alreadyKilled.add(pid);
|
|
75
|
+
killed++;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
/* race: already gone */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return killed;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Sweep stray gate + cloudflared processes for ONE instance, identified by its
|
|
85
|
+
* gate port. Both signatures are port-scoped so tearing down one tunnel never
|
|
86
|
+
* touches another:
|
|
87
|
+
* - gate: `gate.ts ... --port <port>` (the port is on the gate's argv)
|
|
88
|
+
* - cloudflared: `--url http://localhost:<port>` (order-independent, so it
|
|
89
|
+
* matches regardless of the `--protocol http2` flag we also pass).
|
|
90
|
+
* Port boundary is guarded with `( |$)` so port 9001 doesn't match 90011.
|
|
91
|
+
*/
|
|
92
|
+
function sweepStrays(gatePort, alreadyKilled) {
|
|
93
|
+
return (killByPattern(`gate\\.ts.*--port ${gatePort}( |$)`, alreadyKilled) +
|
|
94
|
+
killByPattern(`--url http://localhost:${gatePort}( |$)`, alreadyKilled));
|
|
95
|
+
}
|
|
96
|
+
/** Ports currently bound by a LISTEN socket (best-effort via `ss`). */
|
|
97
|
+
function listeningPorts() {
|
|
98
|
+
const ports = new Set();
|
|
99
|
+
const r = spawnSync("ss", ["-tlnH"], { encoding: "utf-8" });
|
|
100
|
+
if (r.status === 0 && typeof r.stdout === "string") {
|
|
101
|
+
for (const m of r.stdout.matchAll(/:(\d+)\s/g))
|
|
102
|
+
ports.add(Number(m[1]));
|
|
103
|
+
}
|
|
104
|
+
return ports;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Pick a gate port for a new instance. An explicit `--gate-port` is honored
|
|
108
|
+
* (and rejected if it's already taken); otherwise scan upward from 9001 for the
|
|
109
|
+
* first port that's neither held by a live instance nor currently listening.
|
|
110
|
+
*/
|
|
111
|
+
function allocateGatePort(preferred) {
|
|
112
|
+
const used = new Set(listStates()
|
|
113
|
+
.filter((s) => isProcessAlive(s.gate_pid))
|
|
114
|
+
.map((s) => s.gate_port));
|
|
115
|
+
const listening = listeningPorts();
|
|
116
|
+
const taken = (p) => used.has(p) || listening.has(p);
|
|
117
|
+
if (preferred !== undefined) {
|
|
118
|
+
if (taken(preferred)) {
|
|
119
|
+
emit.error({
|
|
120
|
+
code: "tunnel_port_taken",
|
|
121
|
+
message: `Gate port ${preferred} is already in use. Omit --gate-port to auto-allocate, or pick a free one.`,
|
|
122
|
+
});
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
return preferred;
|
|
126
|
+
}
|
|
127
|
+
for (let p = DEFAULT_GATE_PORT; p <= MAX_GATE_PORT; p++) {
|
|
128
|
+
if (!taken(p))
|
|
129
|
+
return p;
|
|
130
|
+
}
|
|
131
|
+
emit.error({
|
|
132
|
+
code: "tunnel_no_free_port",
|
|
133
|
+
message: `No free gate port in ${DEFAULT_GATE_PORT}-${MAX_GATE_PORT}. Tear down some tunnels first.`,
|
|
134
|
+
});
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
function extractUrl(log) {
|
|
138
|
+
const m = log.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
139
|
+
return m ? m[0] : null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* cloudflared logs "Registered tunnel connection" once the edge is live and
|
|
143
|
+
* routable. Match that exact line only, because earlier lines carry `connIndex=`
|
|
144
|
+
* too (e.g. "Tunnel connection curve preferences … connIndex=0"), which would
|
|
145
|
+
* false-positive readiness before the connection actually registers.
|
|
146
|
+
*/
|
|
147
|
+
function isRegistered(log) {
|
|
148
|
+
return /Registered tunnel connection/.test(log);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Wait for the tunnel to be genuinely usable. cloudflared prints the
|
|
152
|
+
* `*.trycloudflare.com` URL early (at precheck) but the hostname doesn't route
|
|
153
|
+
* until the edge connection is *registered*, a few seconds later, and
|
|
154
|
+
* occasionally never on a wedged QUIC start. We gate readiness on the
|
|
155
|
+
* registration line, not just the URL, so `up` doesn't hand back a URL that
|
|
156
|
+
* 404s/times out. Returns the URL (if seen at all) plus whether it registered.
|
|
157
|
+
*/
|
|
158
|
+
async function waitForReady(logPath, timeoutMs) {
|
|
159
|
+
const deadline = Date.now() + timeoutMs;
|
|
160
|
+
let url = null;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
if (existsSync(logPath)) {
|
|
163
|
+
const log = readFileSync(logPath, "utf-8");
|
|
164
|
+
url = url ?? extractUrl(log);
|
|
165
|
+
if (url && isRegistered(log))
|
|
166
|
+
return { url, registered: true };
|
|
167
|
+
}
|
|
168
|
+
await sleep(500);
|
|
169
|
+
}
|
|
170
|
+
return { url, registered: false };
|
|
171
|
+
}
|
|
172
|
+
/** Resolve the context-supplied default vhost (literal or lazy resolver). */
|
|
173
|
+
function contextVhost() {
|
|
174
|
+
const v = context?.tunnelDefaultVhost;
|
|
175
|
+
const resolved = typeof v === "function" ? v() : v;
|
|
176
|
+
return resolved ?? null;
|
|
177
|
+
}
|
|
178
|
+
async function up(opts) {
|
|
179
|
+
if (!bunAvailable()) {
|
|
180
|
+
emit.error({
|
|
181
|
+
code: "tunnel_requires_bun",
|
|
182
|
+
message: "harn tunnel requires Bun: the gate worker is a Bun.serve process. " +
|
|
183
|
+
"Install Bun (https://bun.sh) and re-run. (Every other harn command runs on Node.)",
|
|
184
|
+
});
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
const name = resolveName(opts.name);
|
|
188
|
+
const target = opts.target ?? DEFAULT_TARGET;
|
|
189
|
+
// Precedence: explicit --vhost > the consumer's configured default (via
|
|
190
|
+
// context.tunnelDefaultVhost) > "localhost".
|
|
191
|
+
const vhost = opts.vhost ?? contextVhost() ?? DEFAULT_VHOST;
|
|
192
|
+
const existing = readState(name);
|
|
193
|
+
if (existing && isProcessAlive(existing.gate_pid) && isProcessAlive(existing.cloudflared_pid)) {
|
|
194
|
+
emit.text(`Already up [${name}]: ${existing.url}\n`);
|
|
195
|
+
emit.text(` Forwarding: ${existing.target} (Host: ${existing.vhost})\n`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (existing)
|
|
199
|
+
clearState(name);
|
|
200
|
+
// Allocate the gate port (after clearing dead state so its old port frees up
|
|
201
|
+
// for reuse). Explicit --gate-port is validated; otherwise auto-scan.
|
|
202
|
+
const gatePort = allocateGatePort(opts.gatePort ? Number(opts.gatePort) : undefined);
|
|
203
|
+
// Self-heal: clear any orphaned gate/cloudflared on THIS instance's port from
|
|
204
|
+
// a prior crashed or state-cleared run so the gate port is free before we bind.
|
|
205
|
+
if (sweepStrays(gatePort, new Set()))
|
|
206
|
+
await sleep(500);
|
|
207
|
+
const cfg = readConfig();
|
|
208
|
+
if (cfg.allowed_ips.length === 0) {
|
|
209
|
+
emit.error({
|
|
210
|
+
code: "tunnel_allowlist_empty",
|
|
211
|
+
message: "Allowlist is empty; refusing to start. Add an IP first: harn tunnel allow add <ip>",
|
|
212
|
+
});
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
const cloudflaredBin = ensureCloudflared();
|
|
216
|
+
const gateLogPath = cachePath("tunnel", gateLogFile(name));
|
|
217
|
+
const cfdLogPath = cachePath("tunnel", cfdLogFile(name));
|
|
218
|
+
writeFileSync(gateLogPath, "");
|
|
219
|
+
writeFileSync(cfdLogPath, "");
|
|
220
|
+
const gateFd = openSync(gateLogPath, "a");
|
|
221
|
+
// `--name`/`--port` on argv mirror the env vars; they're what makes the gate
|
|
222
|
+
// process distinguishable per-instance in `pgrep -f` (see sweepStrays).
|
|
223
|
+
const gateProc = spawn("bun", ["run", gateScriptPath(), "--name", name, "--port", String(gatePort)], {
|
|
224
|
+
detached: true,
|
|
225
|
+
stdio: ["ignore", gateFd, gateFd],
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
HARNERY_TUNNEL_ALLOW: cfg.allowed_ips.join(","),
|
|
229
|
+
HARNERY_TUNNEL_TARGET: target,
|
|
230
|
+
HARNERY_TUNNEL_VHOST: vhost,
|
|
231
|
+
HARNERY_TUNNEL_PORT: String(gatePort),
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
gateProc.unref();
|
|
235
|
+
await sleep(800);
|
|
236
|
+
if (!isProcessAlive(gateProc.pid)) {
|
|
237
|
+
emit.error({
|
|
238
|
+
code: "tunnel_gate_failed",
|
|
239
|
+
message: `Gate failed to start. Check log: ${gateLogPath}`,
|
|
240
|
+
});
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
const cfdFd = openSync(cfdLogPath, "a");
|
|
244
|
+
// Force HTTP/2 transport. The default QUIC transport wedges at precheck on
|
|
245
|
+
// constrained hosts (e.g. WSL, where UDP receive buffers can't grow and ICMP
|
|
246
|
+
// is restricted): the URL prints but the edge never registers. HTTP/2 is
|
|
247
|
+
// marginally higher-latency but registers reliably, which is what a dev
|
|
248
|
+
// tunnel needs.
|
|
249
|
+
const cfdProc = spawn(cloudflaredBin, ["tunnel", "--protocol", "http2", "--url", `http://localhost:${gatePort}`], {
|
|
250
|
+
detached: true,
|
|
251
|
+
stdio: ["ignore", cfdFd, cfdFd],
|
|
252
|
+
});
|
|
253
|
+
cfdProc.unref();
|
|
254
|
+
const { url, registered } = await waitForReady(cfdLogPath, 30_000);
|
|
255
|
+
if (!url) {
|
|
256
|
+
try {
|
|
257
|
+
process.kill(gateProc.pid);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
/* already dead */
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
process.kill(cfdProc.pid);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
/* already dead */
|
|
267
|
+
}
|
|
268
|
+
emit.error({
|
|
269
|
+
code: "tunnel_url_timeout",
|
|
270
|
+
message: `Failed to obtain tunnel URL within 30s. Check log: ${cfdLogPath}`,
|
|
271
|
+
});
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
const state = {
|
|
275
|
+
name,
|
|
276
|
+
url,
|
|
277
|
+
gate_pid: gateProc.pid,
|
|
278
|
+
cloudflared_pid: cfdProc.pid,
|
|
279
|
+
started_at: new Date().toISOString(),
|
|
280
|
+
target,
|
|
281
|
+
vhost,
|
|
282
|
+
gate_port: gatePort,
|
|
283
|
+
};
|
|
284
|
+
writeState(state);
|
|
285
|
+
const stopHint = name === DEFAULT_INSTANCE ? "harn tunnel down" : `harn tunnel down --name ${name}`;
|
|
286
|
+
emit.text(`\n Instance: ${name}\n`);
|
|
287
|
+
emit.text(` URL: ${url}\n\n`);
|
|
288
|
+
emit.text(` Forwarding: ${target} (Host: ${vhost})\n`);
|
|
289
|
+
emit.text(` Gate port: ${gatePort}\n`);
|
|
290
|
+
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n\n`);
|
|
291
|
+
if (!registered) {
|
|
292
|
+
emit.text(` ⚠ Edge connection didn't register within 30s (QUIC can wedge on a cold\n start). If the URL 404s or times out, bounce it: ${stopHint} && harn tunnel up\n\n`);
|
|
293
|
+
}
|
|
294
|
+
emit.text(` Stop: ${stopHint}\n`);
|
|
295
|
+
emit.text(" Status: harn tunnel status\n");
|
|
296
|
+
}
|
|
297
|
+
/** Tear down a single instance by name. Returns the number of processes killed. */
|
|
298
|
+
function downOne(name, killed) {
|
|
299
|
+
const before = killed.size;
|
|
300
|
+
const state = readState(name);
|
|
301
|
+
if (state) {
|
|
302
|
+
for (const pid of [state.gate_pid, state.cloudflared_pid]) {
|
|
303
|
+
if (isProcessAlive(pid)) {
|
|
304
|
+
try {
|
|
305
|
+
process.kill(pid);
|
|
306
|
+
killed.add(pid);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
/* race: already gone */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Fallback: sweep orphans on this instance's gate port, even when state was
|
|
315
|
+
// lost; they'd otherwise squat on the port and break the next `up`.
|
|
316
|
+
sweepStrays(state?.gate_port ?? DEFAULT_GATE_PORT, killed);
|
|
317
|
+
clearState(name);
|
|
318
|
+
return killed.size - before;
|
|
319
|
+
}
|
|
320
|
+
function down(opts) {
|
|
321
|
+
const killed = new Set();
|
|
322
|
+
if (opts.all) {
|
|
323
|
+
const states = listStates();
|
|
324
|
+
if (states.length === 0) {
|
|
325
|
+
emit.text("No tunnels up. Nothing to stop.\n");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
for (const s of states)
|
|
329
|
+
downOne(s.name, killed);
|
|
330
|
+
emit.text(`Stopped ${states.length} tunnel(s) [${states.map((s) => s.name).join(", ")}], ${killed.size} process(es).\n`);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const name = resolveName(opts.name);
|
|
334
|
+
// Bare `down` targets the default instance. If it's not up but named ones
|
|
335
|
+
// are, don't silently no-op; point the operator at them.
|
|
336
|
+
if (name === DEFAULT_INSTANCE && !readState(DEFAULT_INSTANCE)) {
|
|
337
|
+
const others = listStates();
|
|
338
|
+
if (others.length > 0) {
|
|
339
|
+
emit.text(`No default tunnel running. Other tunnels up: ${others.map((s) => s.name).join(", ")}.\nUse \`harn tunnel down --name <name>\` or \`harn tunnel down --all\`.\n`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
downOne(name, killed);
|
|
344
|
+
emit.text(killed.size === 0
|
|
345
|
+
? `No tunnel processes found for [${name}]. Nothing to stop.\n`
|
|
346
|
+
: `Stopped ${killed.size} process(es). Tunnel [${name}] down.\n`);
|
|
347
|
+
}
|
|
348
|
+
function instanceState(state) {
|
|
349
|
+
return isProcessAlive(state.gate_pid) && isProcessAlive(state.cloudflared_pid) ? "up" : "stale";
|
|
350
|
+
}
|
|
351
|
+
function fmtUptime(startedAt) {
|
|
352
|
+
const secs = Math.floor((Date.now() - new Date(startedAt).getTime()) / 1000);
|
|
353
|
+
if (!Number.isFinite(secs) || secs < 0)
|
|
354
|
+
return "?";
|
|
355
|
+
if (secs < 60)
|
|
356
|
+
return `${secs}s`;
|
|
357
|
+
if (secs < 3600)
|
|
358
|
+
return `${Math.floor(secs / 60)}m`;
|
|
359
|
+
return `${Math.floor(secs / 3600)}h${Math.floor((secs % 3600) / 60)}m`;
|
|
360
|
+
}
|
|
361
|
+
/** Detailed single-instance block (the pre-multi-instance format). */
|
|
362
|
+
function statusDetail(state) {
|
|
363
|
+
const gateAlive = isProcessAlive(state.gate_pid);
|
|
364
|
+
const cfdAlive = isProcessAlive(state.cloudflared_pid);
|
|
365
|
+
const cfg = readConfig();
|
|
366
|
+
emit.text(`${gateAlive && cfdAlive ? "up" : "stale"} [${state.name}]\n`);
|
|
367
|
+
emit.text(` URL: ${state.url}\n`);
|
|
368
|
+
emit.text(` Forwarding: ${state.target} (Host: ${state.vhost})\n`);
|
|
369
|
+
emit.text(` Gate port: ${state.gate_port}\n`);
|
|
370
|
+
emit.text(` Allowed IPs: ${cfg.allowed_ips.join(", ")}\n`);
|
|
371
|
+
emit.text(` Gate PID: ${state.gate_pid}${gateAlive ? "" : " (DEAD)"}\n`);
|
|
372
|
+
emit.text(` CFD PID: ${state.cloudflared_pid}${cfdAlive ? "" : " (DEAD)"}\n`);
|
|
373
|
+
emit.text(` Uptime: ${fmtUptime(state.started_at)}\n`);
|
|
374
|
+
}
|
|
375
|
+
function status(opts) {
|
|
376
|
+
// Named → detailed single block.
|
|
377
|
+
if (opts.name) {
|
|
378
|
+
const state = readState(resolveName(opts.name));
|
|
379
|
+
if (!state) {
|
|
380
|
+
emit.text(`down [${resolveName(opts.name)}]\n`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
statusDetail(state);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
// No name → table of every instance.
|
|
387
|
+
const states = listStates();
|
|
388
|
+
if (states.length === 0) {
|
|
389
|
+
emit.text("down\n");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (states.length === 1) {
|
|
393
|
+
// Single tunnel: show the full detail block (backward-compatible).
|
|
394
|
+
statusDetail(states[0]);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const rows = states.map((s) => ({
|
|
398
|
+
name: s.name,
|
|
399
|
+
state: instanceState(s),
|
|
400
|
+
url: s.url,
|
|
401
|
+
fwd: `${s.target} (${s.vhost})`,
|
|
402
|
+
port: String(s.gate_port),
|
|
403
|
+
up: fmtUptime(s.started_at),
|
|
404
|
+
}));
|
|
405
|
+
const w = {
|
|
406
|
+
name: Math.max(4, ...rows.map((r) => r.name.length)),
|
|
407
|
+
state: 5,
|
|
408
|
+
url: Math.max(3, ...rows.map((r) => r.url.length)),
|
|
409
|
+
fwd: Math.max(10, ...rows.map((r) => r.fwd.length)),
|
|
410
|
+
port: 4,
|
|
411
|
+
};
|
|
412
|
+
const pad = (s, n) => s.padEnd(n);
|
|
413
|
+
emit.text(`${pad("NAME", w.name)} ${pad("STATE", w.state)} ${pad("URL", w.url)} ${pad("FORWARDING", w.fwd)} ${pad("PORT", w.port)} UPTIME\n`);
|
|
414
|
+
for (const r of rows) {
|
|
415
|
+
emit.text(`${pad(r.name, w.name)} ${pad(r.state, w.state)} ${pad(r.url, w.url)} ${pad(r.fwd, w.fwd)} ${pad(r.port, w.port)} ${r.up}\n`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function logs(opts) {
|
|
419
|
+
const name = resolveName(opts.name);
|
|
420
|
+
const which = opts.cloudflared ? cfdLogFile(name) : gateLogFile(name);
|
|
421
|
+
const path = cachePath("tunnel", which);
|
|
422
|
+
if (!existsSync(path)) {
|
|
423
|
+
emit.error({ code: "tunnel_no_log", message: `No log file at ${path}` });
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
const args = opts.follow ? ["-f", path] : [path];
|
|
427
|
+
const r = spawnSync("tail", args, { stdio: "inherit" });
|
|
428
|
+
if (r.status !== null && r.status !== 0)
|
|
429
|
+
process.exit(r.status);
|
|
430
|
+
}
|
|
431
|
+
function allowList() {
|
|
432
|
+
const cfg = readConfig();
|
|
433
|
+
if (cfg.allowed_ips.length === 0) {
|
|
434
|
+
emit.text("(empty)\n");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
for (const ip of cfg.allowed_ips)
|
|
438
|
+
emit.text(`${ip}\n`);
|
|
439
|
+
}
|
|
440
|
+
function allowAdd(ip) {
|
|
441
|
+
const cfg = readConfig();
|
|
442
|
+
if (cfg.allowed_ips.includes(ip)) {
|
|
443
|
+
emit.text(`${ip} already in allowlist.\n`);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
cfg.allowed_ips.push(ip);
|
|
447
|
+
writeConfig(cfg);
|
|
448
|
+
emit.text(`Added ${ip}.\n`);
|
|
449
|
+
const up = listStates();
|
|
450
|
+
if (up.length > 0) {
|
|
451
|
+
emit.text(`Allowlist is shared across all tunnels; restart each to apply (${up.map((s) => s.name).join(", ")}).\n`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function allowRm(ip) {
|
|
455
|
+
const cfg = readConfig();
|
|
456
|
+
const idx = cfg.allowed_ips.indexOf(ip);
|
|
457
|
+
if (idx === -1) {
|
|
458
|
+
emit.text(`${ip} not in allowlist.\n`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
cfg.allowed_ips.splice(idx, 1);
|
|
462
|
+
writeConfig(cfg);
|
|
463
|
+
emit.text(`Removed ${ip}.\n`);
|
|
464
|
+
const up = listStates();
|
|
465
|
+
if (up.length > 0) {
|
|
466
|
+
emit.text(`Allowlist is shared across all tunnels; restart each to apply (${up.map((s) => s.name).join(", ")}).\n`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
let emit;
|
|
470
|
+
let context;
|
|
471
|
+
export function registerTunnelCommand(program, emitParam, contextParam) {
|
|
472
|
+
emit = emitParam;
|
|
473
|
+
context = contextParam;
|
|
474
|
+
const cmd = program
|
|
475
|
+
.command("tunnel")
|
|
476
|
+
.description("IP-gated Cloudflare quick tunnel(s) in front of a local upstream (default upstream: 127.0.0.1:8001). " +
|
|
477
|
+
"Run several at once with --name <instance>.");
|
|
478
|
+
cmd
|
|
479
|
+
.command("up")
|
|
480
|
+
.description("Start a gate + cloudflared tunnel (one per --name instance)")
|
|
481
|
+
.option("--name <name>", "instance name; run multiple tunnels side by side", DEFAULT_INSTANCE)
|
|
482
|
+
.option("--target <addr>", "upstream to forward to", DEFAULT_TARGET)
|
|
483
|
+
.option("--vhost <host>", "Host header sent to the upstream (default: the consumer's configured " +
|
|
484
|
+
"default, else localhost).")
|
|
485
|
+
.option("--gate-port <port>", "local port the gate binds to (default: auto-allocate the first free port from 9001)")
|
|
486
|
+
.action(up);
|
|
487
|
+
cmd
|
|
488
|
+
.command("down")
|
|
489
|
+
.description("Stop a tunnel (default instance, --name <instance>, or --all)")
|
|
490
|
+
.option("--name <name>", "instance to stop", DEFAULT_INSTANCE)
|
|
491
|
+
.option("--all", "stop every running tunnel")
|
|
492
|
+
.action(down);
|
|
493
|
+
cmd
|
|
494
|
+
.command("status")
|
|
495
|
+
.description("Show tunnel state: a table of all instances, or detail for one via --name")
|
|
496
|
+
.option("--name <name>", "show detail for a single instance")
|
|
497
|
+
.action(status);
|
|
498
|
+
cmd
|
|
499
|
+
.command("logs")
|
|
500
|
+
.description("Tail the gate log (default) or cloudflared log for an instance")
|
|
501
|
+
.option("--name <name>", "instance whose log to tail", DEFAULT_INSTANCE)
|
|
502
|
+
.option("-f, --follow", "follow the log")
|
|
503
|
+
.option("--gate", "tail the gate log (default)")
|
|
504
|
+
.option("--cloudflared", "tail the cloudflared log instead")
|
|
505
|
+
.action(logs);
|
|
506
|
+
const allow = cmd
|
|
507
|
+
.command("allow")
|
|
508
|
+
.description("Manage the CF-Connecting-IP allowlist")
|
|
509
|
+
.action(allowList);
|
|
510
|
+
allow.command("add <ip>").description("Add an IP to the allowlist").action(allowAdd);
|
|
511
|
+
allow.command("rm <ip>").description("Remove an IP from the allowlist").action(allowRm);
|
|
512
|
+
allow.command("list").description("List allowed IPs (default action)").action(allowList);
|
|
513
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harn uninstall`: reverse what `harn init` wired into a project.
|
|
3
|
+
*
|
|
4
|
+
* `init` makes two kinds of change outside the harnery package:
|
|
5
|
+
* 1. Merges `agent-hook` entries into the harness settings file
|
|
6
|
+
* (Claude Code `.claude/settings.json`, Cursor `.cursor/hooks.json`, or
|
|
7
|
+
* Codex `.codex/hooks.json`).
|
|
8
|
+
* 2. Creates the `.harnery/` coord root (runtime state: events, councils,
|
|
9
|
+
* identities, scratch) and stamps the host bin name into
|
|
10
|
+
* `.harnery/config.jsonc`.
|
|
11
|
+
*
|
|
12
|
+
* `uninstall` undoes (1) by default: it removes only harnery's hook entries from
|
|
13
|
+
* the settings file, preserving any other hooks the consumer added, and deletes
|
|
14
|
+
* the settings file outright when it's left harnery-only. It does NOT touch the
|
|
15
|
+
* `.harnery/` coord root unless `--purge-state` is passed, because that directory
|
|
16
|
+
* holds session history a consumer may want to keep. Idempotent + `--dry-run`,
|
|
17
|
+
* mirroring `init`.
|
|
18
|
+
*/
|
|
19
|
+
import type { Command } from "commander";
|
|
20
|
+
import type { EmitContext } from "../commander.js";
|
|
21
|
+
export declare function registerUninstallCommand(program: Command, emit: EmitContext): void;
|
|
22
|
+
//# sourceMappingURL=uninstall.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uninstall.d.ts","sourceRoot":"","sources":["../../src/commands/uninstall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAWnD,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAoFlF"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `harn uninstall`: reverse what `harn init` wired into a project.
|
|
3
|
+
*
|
|
4
|
+
* `init` makes two kinds of change outside the harnery package:
|
|
5
|
+
* 1. Merges `agent-hook` entries into the harness settings file
|
|
6
|
+
* (Claude Code `.claude/settings.json`, Cursor `.cursor/hooks.json`, or
|
|
7
|
+
* Codex `.codex/hooks.json`).
|
|
8
|
+
* 2. Creates the `.harnery/` coord root (runtime state: events, councils,
|
|
9
|
+
* identities, scratch) and stamps the host bin name into
|
|
10
|
+
* `.harnery/config.jsonc`.
|
|
11
|
+
*
|
|
12
|
+
* `uninstall` undoes (1) by default: it removes only harnery's hook entries from
|
|
13
|
+
* the settings file, preserving any other hooks the consumer added, and deletes
|
|
14
|
+
* the settings file outright when it's left harnery-only. It does NOT touch the
|
|
15
|
+
* `.harnery/` coord root unless `--purge-state` is passed, because that directory
|
|
16
|
+
* holds session history a consumer may want to keep. Idempotent + `--dry-run`,
|
|
17
|
+
* mirroring `init`.
|
|
18
|
+
*/
|
|
19
|
+
import { spawnSync } from "node:child_process";
|
|
20
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { relative, resolve } from "node:path";
|
|
22
|
+
import { HARNESS_SPECS } from "../core/hooks/harness/events.js";
|
|
23
|
+
import { unwireHooks } from "./init.js";
|
|
24
|
+
export function registerUninstallCommand(program, emit) {
|
|
25
|
+
program
|
|
26
|
+
.command("uninstall")
|
|
27
|
+
.description("Reverse `harn init`: remove harnery's hook entries from the harness " +
|
|
28
|
+
"settings file (keeps any others). Pass --purge-state to also delete the " +
|
|
29
|
+
".harnery/ coord root. Idempotent; use --dry-run to preview.")
|
|
30
|
+
.option("--harness <id>", "claude-code | cursor | codex", "claude-code")
|
|
31
|
+
.option("--dry-run", "Show what would change without writing")
|
|
32
|
+
.option("--project-root <path>", "Project root (default: git toplevel, else cwd)")
|
|
33
|
+
.option("--purge-state", "Also delete the .harnery/ coord root (runtime state, destructive)")
|
|
34
|
+
.action((opts) => {
|
|
35
|
+
const harness = opts.harness;
|
|
36
|
+
const spec = HARNESS_SPECS[harness];
|
|
37
|
+
if (!spec) {
|
|
38
|
+
emit.text(`Unknown harness '${opts.harness}'. Expected: claude-code | cursor | codex.`);
|
|
39
|
+
emit.setExitCode(1);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const projectRoot = resolve(opts.projectRoot ?? gitTopLevel() ?? process.cwd());
|
|
43
|
+
const dryRun = opts.dryRun === true;
|
|
44
|
+
const actions = [];
|
|
45
|
+
// ── 1. unwire harness hooks ────────────────────────────────────────────
|
|
46
|
+
const settingsPath = resolve(projectRoot, spec.settingsFile);
|
|
47
|
+
if (!existsSync(settingsPath)) {
|
|
48
|
+
actions.push(`· ${rel(projectRoot, settingsPath)} doesn't exist; no hooks to remove`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
let settings;
|
|
52
|
+
try {
|
|
53
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
emit.text(`✗ ${rel(projectRoot, settingsPath)} exists but isn't valid JSON; refusing to ` +
|
|
57
|
+
`touch it. Fix it and re-run.\n (${err.message})`);
|
|
58
|
+
emit.setExitCode(1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { removed, remaining } = unwireHooks(settings);
|
|
62
|
+
if (removed === 0) {
|
|
63
|
+
actions.push(`· no harnery hooks found in ${rel(projectRoot, settingsPath)}`);
|
|
64
|
+
}
|
|
65
|
+
else if (harnessOnly(settings)) {
|
|
66
|
+
// Nothing left but what init itself put there (no hooks, at most a
|
|
67
|
+
// version key), so remove the file rather than leave an empty shell.
|
|
68
|
+
if (dryRun) {
|
|
69
|
+
actions.push(`+ would remove ${rel(projectRoot, settingsPath)} (now harnery-only)`);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
rmSync(settingsPath);
|
|
73
|
+
actions.push(`+ removed ${rel(projectRoot, settingsPath)} (was harnery-only)`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else if (dryRun) {
|
|
77
|
+
actions.push(`+ would remove ${removed} harnery hook(s) from ${rel(projectRoot, settingsPath)} ` +
|
|
78
|
+
`(${remaining} other hook(s) kept)`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
82
|
+
actions.push(`+ removed ${removed} harnery hook(s) from ${rel(projectRoot, settingsPath)} ` +
|
|
83
|
+
`(${remaining} other hook(s) kept)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// ── 2. coord root (opt-in; destructive) ────────────────────────────────
|
|
87
|
+
const coordDir = resolve(projectRoot, ".harnery");
|
|
88
|
+
if (opts.purgeState) {
|
|
89
|
+
if (!existsSync(coordDir)) {
|
|
90
|
+
actions.push("· .harnery/ doesn't exist; nothing to purge");
|
|
91
|
+
}
|
|
92
|
+
else if (dryRun) {
|
|
93
|
+
actions.push("+ would delete .harnery/ and all coord state (events, councils, scratch)");
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
rmSync(coordDir, { recursive: true, force: true });
|
|
97
|
+
actions.push("+ deleted .harnery/ and all coord state");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else if (existsSync(coordDir)) {
|
|
101
|
+
actions.push("· left .harnery/ coord root in place (pass --purge-state to delete it)");
|
|
102
|
+
}
|
|
103
|
+
emit.text(render(projectRoot, dryRun, actions));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** True when an unwired settings object holds nothing but (optionally) `version`. */
|
|
107
|
+
function harnessOnly(settings) {
|
|
108
|
+
const keys = Object.keys(settings);
|
|
109
|
+
return keys.length === 0 || (keys.length === 1 && keys[0] === "version");
|
|
110
|
+
}
|
|
111
|
+
function render(projectRoot, dryRun, actions) {
|
|
112
|
+
const head = dryRun ? "harn uninstall (dry run): no changes written" : "harn uninstall";
|
|
113
|
+
const tail = dryRun
|
|
114
|
+
? "\nRe-run without --dry-run to apply."
|
|
115
|
+
: "\nDone. harnery hooks are unwired; restart your harness session to drop them.";
|
|
116
|
+
return `${head}\n root: ${projectRoot}\n${actions.map((a) => ` ${a}`).join("\n")}${tail}`;
|
|
117
|
+
}
|
|
118
|
+
function gitTopLevel() {
|
|
119
|
+
const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
|
|
120
|
+
const out = r.status === 0 ? r.stdout.trim() : "";
|
|
121
|
+
return out || null;
|
|
122
|
+
}
|
|
123
|
+
function rel(root, p) {
|
|
124
|
+
const r = relative(root, p);
|
|
125
|
+
return r || p;
|
|
126
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/commands/web.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAsDnD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAqI5E"}
|