@bigknoxy/hashpilot 4.6.3
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 +777 -0
- package/docs/ADAPTER-CONTRACT.md +1260 -0
- package/docs/ARCHITECTURE.md +846 -0
- package/docs/CLI-QUICKREF.md +827 -0
- package/docs/COMPETITIVE-ANALYSIS.md +307 -0
- package/docs/INSTALL.md +403 -0
- package/docs/INTEGRATION-CLAUDE.md +126 -0
- package/docs/INTEGRATION-MCP.md +196 -0
- package/docs/INTEGRATION-OPENCODE.md +136 -0
- package/docs/INTEGRATION-PI.md +195 -0
- package/package.json +77 -0
- package/scripts/build-site.sh +39 -0
- package/scripts/doctor.sh +218 -0
- package/scripts/gen-cli-quickref.ts +232 -0
- package/scripts/install-cli.sh +60 -0
- package/scripts/install.sh +466 -0
- package/scripts/roadmap-lint.ts +200 -0
- package/scripts/uninstall.sh +202 -0
- package/src/cli-node.cjs +51 -0
- package/src/cli.ts +209 -0
- package/src/commands/ast.ts +255 -0
- package/src/commands/diff.ts +98 -0
- package/src/commands/edit.ts +93 -0
- package/src/commands/hash.ts +64 -0
- package/src/commands/intent.ts +68 -0
- package/src/commands/maintenance.ts +191 -0
- package/src/commands/mcp.ts +28 -0
- package/src/commands/provenance.ts +111 -0
- package/src/commands/read.ts +117 -0
- package/src/commands/route.ts +42 -0
- package/src/commands/shared.ts +65 -0
- package/src/commands/telemetry.ts +126 -0
- package/src/commands/verify.ts +61 -0
- package/src/core/ast-edit.ts +2357 -0
- package/src/core/batch-edit.ts +185 -0
- package/src/core/config.ts +189 -0
- package/src/core/diff-engine.ts +474 -0
- package/src/core/doctor.ts +303 -0
- package/src/core/encoding.ts +116 -0
- package/src/core/envelope.ts +163 -0
- package/src/core/exit-codes.ts +198 -0
- package/src/core/format.ts +339 -0
- package/src/core/grep.ts +180 -0
- package/src/core/hash-edit.ts +416 -0
- package/src/core/index.ts +155 -0
- package/src/core/intent.ts +584 -0
- package/src/core/locking.ts +292 -0
- package/src/core/module-system.ts +142 -0
- package/src/core/operations.ts +557 -0
- package/src/core/output.ts +122 -0
- package/src/core/path-normalize.ts +61 -0
- package/src/core/paths.ts +326 -0
- package/src/core/plan-executor.ts +437 -0
- package/src/core/platform.ts +132 -0
- package/src/core/provenance.ts +214 -0
- package/src/core/read.ts +111 -0
- package/src/core/redact.ts +98 -0
- package/src/core/resolve-content.ts +12 -0
- package/src/core/router.ts +463 -0
- package/src/core/snapshot.ts +346 -0
- package/src/core/telemetry.ts +838 -0
- package/src/core/utils.ts +7 -0
- package/src/core/verify-baseline.ts +186 -0
- package/src/core/verify-scope.ts +282 -0
- package/src/core/verify.ts +753 -0
- package/src/mcp/server.ts +325 -0
- package/templates/claude-section.md +12 -0
- package/templates/opencode-agent.md +106 -0
- package/templates/opencode-skill.md +241 -0
- package/templates/pi-extension.ts +288 -0
- package/templates/pi-skill.md +123 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
BOLD='\033[1m'
|
|
5
|
+
DIM='\033[2m'
|
|
6
|
+
GREEN='\033[0;32m'
|
|
7
|
+
YELLOW='\033[0;33m'
|
|
8
|
+
RED='\033[0;31m'
|
|
9
|
+
NC='\033[0m'
|
|
10
|
+
|
|
11
|
+
log() { printf "${GREEN}[hashpilot]${NC} %s\n" "$1"; }
|
|
12
|
+
warn() { printf "${YELLOW}[hashpilot]${NC} %s\n" "$1"; }
|
|
13
|
+
err() { printf "${RED}[hashpilot]${NC} %s\n" "$1"; }
|
|
14
|
+
detail() { printf "${DIM} →${NC} %s\n" "$1"; }
|
|
15
|
+
|
|
16
|
+
TARGET_DIR="${HASHPILOT_DIR:-${HOME}/.agentic-tools}"
|
|
17
|
+
KEEP_CONFIG=false
|
|
18
|
+
FORCE=false
|
|
19
|
+
|
|
20
|
+
while [[ $# -gt 0 ]]; do
|
|
21
|
+
case "$1" in
|
|
22
|
+
--keep-config) KEEP_CONFIG=true; shift ;;
|
|
23
|
+
--force|-f) FORCE=true; shift ;;
|
|
24
|
+
--help|-h)
|
|
25
|
+
echo "HashPilot Uninstaller"
|
|
26
|
+
echo "Usage: $0 [options]"
|
|
27
|
+
echo " --keep-config Preserve config and telemetry data"
|
|
28
|
+
echo " --force, -f Skip confirmation prompt (auto-detected when piped)"
|
|
29
|
+
echo " --help, -h Show this help"
|
|
30
|
+
echo ""
|
|
31
|
+
echo "One-liner: curl -fsSL https://raw.githubusercontent.com/bigknoxy/HashPilot/main/scripts/uninstall.sh | sh -s -- -f"
|
|
32
|
+
exit 0
|
|
33
|
+
;;
|
|
34
|
+
*) err "Unknown option: $1"; exit 1 ;;
|
|
35
|
+
esac
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
echo "${BOLD}HashPilot Uninstaller${NC}"
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
# ── Confirmation ─────────────────────────────────────────────────────────
|
|
42
|
+
if [[ "$FORCE" != "true" ]]; then
|
|
43
|
+
# When piped (no TTY), skip prompt — auto-force
|
|
44
|
+
if ! [[ -t 0 ]]; then
|
|
45
|
+
FORCE=true
|
|
46
|
+
else
|
|
47
|
+
echo "This will remove HashPilot and all its components."
|
|
48
|
+
echo " Target: $TARGET_DIR"
|
|
49
|
+
echo " Keep config: $KEEP_CONFIG"
|
|
50
|
+
echo ""
|
|
51
|
+
echo -n "Continue? [y/N] "
|
|
52
|
+
read -r CONFIRM
|
|
53
|
+
if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then
|
|
54
|
+
log "Uninstall cancelled."
|
|
55
|
+
exit 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
REMOVED=0
|
|
61
|
+
SKIPPED=0
|
|
62
|
+
|
|
63
|
+
remove_file() {
|
|
64
|
+
local path="$1"
|
|
65
|
+
local label="$2"
|
|
66
|
+
# Expand ~ if present
|
|
67
|
+
path="${path/#\~/${HOME}}"
|
|
68
|
+
if [[ -f "$path" ]]; then
|
|
69
|
+
rm -f "$path"
|
|
70
|
+
detail "Removed ${label}: ${path}"
|
|
71
|
+
REMOVED=$((REMOVED+1))
|
|
72
|
+
elif [[ -d "$path" ]]; then
|
|
73
|
+
rm -rf "$path"
|
|
74
|
+
detail "Removed ${label}: ${path}"
|
|
75
|
+
REMOVED=$((REMOVED+1))
|
|
76
|
+
else
|
|
77
|
+
detail "Already removed: ${path}"
|
|
78
|
+
SKIPPED=$((SKIPPED+1))
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# ── Read manifest if available ───────────────────────────────────────────
|
|
83
|
+
MANIFEST="$TARGET_DIR/manifest.json"
|
|
84
|
+
if [[ -f "$MANIFEST" ]]; then
|
|
85
|
+
log "Reading manifest..."
|
|
86
|
+
else
|
|
87
|
+
warn "No manifest found at $MANIFEST — will attempt best-effort cleanup"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# ── Claude integration ───────────────────────────────────────────────────
|
|
91
|
+
log "Removing Claude integration..."
|
|
92
|
+
CLAUDE_FILE="${HOME}/.claude/CLAUDE.md"
|
|
93
|
+
CLAUDE_MARKER_START="HashPilot Claude"
|
|
94
|
+
if [[ -f "$CLAUDE_FILE" ]]; then
|
|
95
|
+
# Remove the HashPilot section: from marker to the next ## or end
|
|
96
|
+
if grep -q "$CLAUDE_MARKER_START" "$CLAUDE_FILE" 2>/dev/null; then
|
|
97
|
+
# Use sed to remove from HashPilot heading to end (or next heading)
|
|
98
|
+
if grep -q "^## " "$CLAUDE_FILE" 2>/dev/null; then
|
|
99
|
+
# Has multiple sections — remove just this one by matching its end
|
|
100
|
+
sed -i '/^## .*HashPilot Claude/,/^## /{
|
|
101
|
+
/^## .*HashPilot Claude/d
|
|
102
|
+
/^## /!d
|
|
103
|
+
}' "$CLAUDE_FILE"
|
|
104
|
+
else
|
|
105
|
+
# Single section — remove from HashPilot heading to end
|
|
106
|
+
sed -i '/^## .*HashPilot Claude/,$d' "$CLAUDE_FILE"
|
|
107
|
+
fi
|
|
108
|
+
# Clean up trailing blank lines + deduplicate mid-file blank lines
|
|
109
|
+
sed -i -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$CLAUDE_FILE" 2>/dev/null || true
|
|
110
|
+
sed -i '/^$/{ N; /^\n$/d }' "$CLAUDE_FILE" 2>/dev/null || true
|
|
111
|
+
detail "Removed HashPilot section from $CLAUDE_FILE"
|
|
112
|
+
REMOVED=$((REMOVED+1))
|
|
113
|
+
else
|
|
114
|
+
detail "HashPilot section not found in $CLAUDE_FILE (skipping)"
|
|
115
|
+
SKIPPED=$((SKIPPED+1))
|
|
116
|
+
fi
|
|
117
|
+
else
|
|
118
|
+
detail "CLAUDE.md not found (skipping)"
|
|
119
|
+
SKIPPED=$((SKIPPED+1))
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# ── OpenCode integration ─────────────────────────────────────────────────
|
|
123
|
+
log "Removing OpenCode integration..."
|
|
124
|
+
remove_file "${HOME}/.config/opencode/skills/hashpilot/SKILL.md" "OpenCode skill"
|
|
125
|
+
remove_file "${HOME}/.config/opencode/agent/hashpilot.md" "OpenCode agent"
|
|
126
|
+
# Remove empty skill/agent dirs
|
|
127
|
+
rmdir "${HOME}/.config/opencode/skills/hashpilot" 2>/dev/null || true
|
|
128
|
+
rmdir "${HOME}/.config/opencode/skills" 2>/dev/null || true
|
|
129
|
+
rmdir "${HOME}/.config/opencode/agent" 2>/dev/null || true
|
|
130
|
+
|
|
131
|
+
# ── Pi integration ───────────────────────────────────────────────────────
|
|
132
|
+
log "Removing Pi integration..."
|
|
133
|
+
remove_file "${HOME}/.pi/agent/extensions/hashpilot.ts" "Pi extension"
|
|
134
|
+
remove_file "${HOME}/.pi/agent/skills/hashpilot/SKILL.md" "Pi skill"
|
|
135
|
+
rmdir "${HOME}/.pi/agent/extensions" 2>/dev/null || true
|
|
136
|
+
rmdir "${HOME}/.pi/agent/skills/hashpilot" 2>/dev/null || true
|
|
137
|
+
rmdir "${HOME}/.pi/agent/skills" 2>/dev/null || true
|
|
138
|
+
|
|
139
|
+
# ── Core, bin, telemetry ─────────────────────────────────────────────────
|
|
140
|
+
log "Removing Core files..."
|
|
141
|
+
remove_file "$TARGET_DIR/bin/hashpilot" "CLI launcher"
|
|
142
|
+
# Also remove stale symlink from old binary name (pre-3.1 installs).
|
|
143
|
+
remove_file "$TARGET_DIR/bin/structured-edit" "stale CLI launcher (old name)"
|
|
144
|
+
rmdir "$TARGET_DIR/bin" 2>/dev/null || true
|
|
145
|
+
|
|
146
|
+
if [[ "$KEEP_CONFIG" == "true" ]]; then
|
|
147
|
+
detail "Preserving telemetry (--keep-config)"
|
|
148
|
+
# Remove core but keep logs
|
|
149
|
+
remove_file "$TARGET_DIR/structured-editing" "Core source"
|
|
150
|
+
# Preserve logs
|
|
151
|
+
detail "Preserving: $TARGET_DIR/logs"
|
|
152
|
+
else
|
|
153
|
+
remove_file "$TARGET_DIR/structured-editing" "Core source"
|
|
154
|
+
remove_file "$TARGET_DIR/logs" "Telemetry logs"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# ── Config ───────────────────────────────────────────────────────────────
|
|
158
|
+
if [[ "$KEEP_CONFIG" == "true" ]]; then
|
|
159
|
+
detail "Preserving config (--keep-config)"
|
|
160
|
+
else
|
|
161
|
+
remove_file "${HOME}/.config/hashpilot/config.json" "Config file"
|
|
162
|
+
rmdir "${HOME}/.config/hashpilot" 2>/dev/null || true
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# ── Remove PATH entry from shell rc ──────────────────────────────────────
|
|
166
|
+
log "Removing PATH entries..."
|
|
167
|
+
for rc in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.bash_profile" "${HOME}/.profile"; do
|
|
168
|
+
if [[ -f "$rc" ]]; then
|
|
169
|
+
if grep -q "# >>> hashpilot path >>>" "$rc" 2>/dev/null; then
|
|
170
|
+
# BSD sed (macOS) requires an explicit backup suffix after -i; GNU sed
|
|
171
|
+
# accepts the empty one, so `-i ''` is not portable either. Pick per-platform.
|
|
172
|
+
if sed --version >/dev/null 2>&1; then SED_INPLACE=(sed -i); else SED_INPLACE=(sed -i ''); fi
|
|
173
|
+
"${SED_INPLACE[@]}" '/^# >>> hashpilot path >>>/,/^# <<< hashpilot path <<</d' "$rc"
|
|
174
|
+
# Clean up trailing blank lines
|
|
175
|
+
"${SED_INPLACE[@]}" -e :a -e '/^\n*$/{$d;N;ba' -e '}' "$rc" 2>/dev/null || true
|
|
176
|
+
detail "Removed PATH entry from $rc"
|
|
177
|
+
REMOVED=$((REMOVED+1))
|
|
178
|
+
fi
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
# ── Remove manifest and target dir if empty ──────────────────────────────
|
|
183
|
+
remove_file "$MANIFEST" "Manifest"
|
|
184
|
+
rmdir "$TARGET_DIR" 2>/dev/null || true
|
|
185
|
+
|
|
186
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
187
|
+
echo ""
|
|
188
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
189
|
+
printf "${GREEN} HashPilot uninstalled${NC}\n"
|
|
190
|
+
printf " Removed: %d Skipped: %d\n" "$REMOVED" "$SKIPPED"
|
|
191
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
192
|
+
echo ""
|
|
193
|
+
echo "Note: The following may remain if they existed before HashPilot:"
|
|
194
|
+
echo " ~/.bashrc modifications (restored)"
|
|
195
|
+
echo " ~/.claude/CLAUDE.md modifications (restored)"
|
|
196
|
+
echo " ~/.pi/agent/extensions/ (if still empty)"
|
|
197
|
+
echo " ~/.config/opencode/skills/hashpilot/ (if still empty)"
|
|
198
|
+
echo ""
|
|
199
|
+
echo "To complete removal, restart your shell or run:"
|
|
200
|
+
echo " hash -r 2>/dev/null || exec \$SHELL"
|
|
201
|
+
echo ""
|
|
202
|
+
|
package/src/cli-node.cjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Node-parseable entry point for the `hashpilot` binary.
|
|
3
|
+
//
|
|
4
|
+
// HashPilot runs on Bun (src/cli.ts has a `#!/usr/bin/env bun` shebang and uses
|
|
5
|
+
// Bun-only APIs). Pointing `bin` straight at the TypeScript source means
|
|
6
|
+
// `npm i -g hashpilot` on a Node-only machine installs cleanly and then dies with
|
|
7
|
+
// a syntax error on first use. This shim is deliberately plain CommonJS so any
|
|
8
|
+
// Node >= 14 can parse it, and its only job is to hand off to Bun — or, when Bun
|
|
9
|
+
// is absent, to say so in one actionable line instead of a stack trace.
|
|
10
|
+
//
|
|
11
|
+
// `.cjs` (not `.js`) because package.json declares `"type": "module"`.
|
|
12
|
+
// See issue #35, Option A.
|
|
13
|
+
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
var path = require("path");
|
|
17
|
+
var spawnSync = require("child_process").spawnSync;
|
|
18
|
+
|
|
19
|
+
var CLI = path.join(__dirname, "cli.ts");
|
|
20
|
+
|
|
21
|
+
// Array argv, never a shell string: arguments routinely contain source code,
|
|
22
|
+
// spaces, quotes, and newlines, and a shell would both mangle and execute them.
|
|
23
|
+
var res = spawnSync("bun", ["run", CLI].concat(process.argv.slice(2)), {
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (res.error && res.error.code === "ENOENT") {
|
|
28
|
+
process.stderr.write(
|
|
29
|
+
"hashpilot requires Bun (>= 1.2.0), which was not found on your PATH.\n" +
|
|
30
|
+
"\n" +
|
|
31
|
+
" Install it: curl -fsSL https://bun.sh/install | bash\n" +
|
|
32
|
+
" Then reopen your shell and re-run this command.\n" +
|
|
33
|
+
"\n" +
|
|
34
|
+
"HashPilot is Bun-only today; see the support matrix in the README.\n"
|
|
35
|
+
);
|
|
36
|
+
process.exit(127);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (res.error) {
|
|
40
|
+
process.stderr.write("hashpilot: failed to launch Bun: " + res.error.message + "\n");
|
|
41
|
+
process.exit(70);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Propagate the child's exit status verbatim. HashPilot's exit codes are a
|
|
45
|
+
// documented contract (0 ok · 1 usage · 2 edit failed · 3 stale · 4 verify
|
|
46
|
+
// failed · 5 I/O · 70 internal); collapsing them to 0/1 would break every
|
|
47
|
+
// caller that branches on code 3 to retry a stale anchor.
|
|
48
|
+
if (res.signal) {
|
|
49
|
+
process.exit(128 + (require("os").constants.signals[res.signal] || 0));
|
|
50
|
+
}
|
|
51
|
+
process.exit(res.status === null ? 70 : res.status);
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command, CommanderError } from "commander";
|
|
3
|
+
// Single source of truth for the version. Bun inlines this JSON import at build
|
|
4
|
+
// time, so dist/ carries the real version instead of a hardcoded literal.
|
|
5
|
+
import pkg from "../package.json" with { type: "json" };
|
|
6
|
+
import {
|
|
7
|
+
loadConfig,
|
|
8
|
+
createChangeSet,
|
|
9
|
+
configureSnapshots,
|
|
10
|
+
setCurrentChangeSet,
|
|
11
|
+
pruneSnapshots,
|
|
12
|
+
PathDeniedError,
|
|
13
|
+
configureWriteBoundary,
|
|
14
|
+
finish,
|
|
15
|
+
usageError,
|
|
16
|
+
setCommand,
|
|
17
|
+
setAllowParseErrors,
|
|
18
|
+
ErrorCode,
|
|
19
|
+
ExitCode,
|
|
20
|
+
configureTelemetry,
|
|
21
|
+
enableTelemetry,
|
|
22
|
+
resolveTelemetryEnabled,
|
|
23
|
+
setOutputFormat,
|
|
24
|
+
configureOutput,
|
|
25
|
+
resolveFormat,
|
|
26
|
+
TelemetryReadError,
|
|
27
|
+
} from "./core/index";
|
|
28
|
+
|
|
29
|
+
// Every command group registers itself onto `program`. The registration order
|
|
30
|
+
// below is the order the groups appear in `--help`; do not reshuffle it (#48).
|
|
31
|
+
import { register as registerRead } from "./commands/read";
|
|
32
|
+
import { register as registerHash } from "./commands/hash";
|
|
33
|
+
import { register as registerAst } from "./commands/ast";
|
|
34
|
+
import { register as registerEdit } from "./commands/edit";
|
|
35
|
+
import { register as registerIntent } from "./commands/intent";
|
|
36
|
+
import { register as registerDiff } from "./commands/diff";
|
|
37
|
+
import { register as registerVerify } from "./commands/verify";
|
|
38
|
+
import { register as registerTelemetry } from "./commands/telemetry";
|
|
39
|
+
import { register as registerProvenance } from "./commands/provenance";
|
|
40
|
+
import { register as registerMcp } from "./commands/mcp";
|
|
41
|
+
import { register as registerMaintenance } from "./commands/maintenance";
|
|
42
|
+
import { register as registerRoute } from "./commands/route";
|
|
43
|
+
|
|
44
|
+
const VERSION: string = pkg.version;
|
|
45
|
+
|
|
46
|
+
const program = new Command();
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.name("hashpilot")
|
|
50
|
+
.description("HashPilot — Structured Editing Core for Coding Agents")
|
|
51
|
+
.version(VERSION)
|
|
52
|
+
.option("--allow-outside-root", "Permit writes outside the project root (credentials and system paths stay blocked)")
|
|
53
|
+
.option("--allowed-root <dir...>", "Additional directory writes may target")
|
|
54
|
+
.option("--no-telemetry", "Disable telemetry logging for this invocation")
|
|
55
|
+
.option("--allow-parse-errors", "Edit a file that already has syntax errors (the post-edit parse check still applies)")
|
|
56
|
+
.option("--format <fmt>", "Output format: json or text (default: json if piped/CI, text if TTY)")
|
|
57
|
+
.option("--json", "[deprecated: use --format json] Force JSON output", false)
|
|
58
|
+
.option("-q, --quiet", "Suppress the human-readable success line (the JSON envelope is never suppressed)")
|
|
59
|
+
.option("-v, --verbose", "Write routing and timing diagnostics to stderr")
|
|
60
|
+
.option("--no-color", "Disable ANSI color in text output (also honors NO_COLOR)")
|
|
61
|
+
.hook("preAction", (thisCommand, actionCommand) => {
|
|
62
|
+
// Name the running subcommand so the envelope can report it. Walk up so
|
|
63
|
+
// nested commands read as "telemetry show", not "show".
|
|
64
|
+
const path: string[] = [];
|
|
65
|
+
for (let c: typeof actionCommand | null = actionCommand; c && c.parent; c = c.parent) path.unshift(c.name());
|
|
66
|
+
setCommand(path.join(" "));
|
|
67
|
+
|
|
68
|
+
// #19 (B16): resolve output format and set it globally
|
|
69
|
+
const globals = thisCommand.opts();
|
|
70
|
+
const { format, warnDeprecate } = resolveFormat(globals, { ci: process.env.CI === "true" || process.env.CI === "1" });
|
|
71
|
+
setOutputFormat(format, path.join(" "));
|
|
72
|
+
// Color and verbosity resolve from the same globals, after the format is
|
|
73
|
+
// known: color is text-mode only, so JSON output can never carry escapes (#47).
|
|
74
|
+
configureOutput({
|
|
75
|
+
quiet: Boolean(globals.quiet),
|
|
76
|
+
verbose: Boolean(globals.verbose),
|
|
77
|
+
color: globals.color,
|
|
78
|
+
format,
|
|
79
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
80
|
+
});
|
|
81
|
+
if (warnDeprecate) process.stderr.write("[deprecation] --json is deprecated; use --format json\n");
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
configureWriteBoundary({
|
|
84
|
+
allowOutsideRoot: Boolean(globals.allowOutsideRoot),
|
|
85
|
+
allowedRoots: [...(config.allowedRoots || []), ...(globals.allowedRoot || [])],
|
|
86
|
+
});
|
|
87
|
+
// Apply sizing/retention from config, then the kill switch, so the CLI flag
|
|
88
|
+
// and env var win over `telemetry.enabled`.
|
|
89
|
+
configureTelemetry(config.telemetry);
|
|
90
|
+
enableTelemetry(resolveTelemetryEnabled(config.telemetry, globals.telemetry === false));
|
|
91
|
+
setAllowParseErrors(Boolean(globals.allowParseErrors));
|
|
92
|
+
|
|
93
|
+
// Every write this invocation makes belongs to one changeSet, so `undo`
|
|
94
|
+
// has a unit to work in even for commands that never mint one themselves.
|
|
95
|
+
configureSnapshots(config.snapshots);
|
|
96
|
+
setCurrentChangeSet(config.snapshots?.enabled === false ? null : createChangeSet());
|
|
97
|
+
pruneSnapshots();
|
|
98
|
+
});
|
|
99
|
+
registerRead(program);
|
|
100
|
+
registerHash(program);
|
|
101
|
+
registerAst(program);
|
|
102
|
+
registerEdit(program);
|
|
103
|
+
registerIntent(program);
|
|
104
|
+
registerDiff(program);
|
|
105
|
+
registerVerify(program);
|
|
106
|
+
registerTelemetry(program);
|
|
107
|
+
registerProvenance(program);
|
|
108
|
+
registerMcp(program);
|
|
109
|
+
registerMaintenance(program);
|
|
110
|
+
registerRoute(program);
|
|
111
|
+
|
|
112
|
+
/** Node syscall codes that mean "the filesystem said no", not "HashPilot has a bug". */
|
|
113
|
+
const IO_SYSCALL_CODES = new Set([
|
|
114
|
+
"ENOENT", "EACCES", "EPERM", "EISDIR", "ENOTDIR", "ENOSPC", "EROFS", "EMFILE", "ENFILE", "EBUSY",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Nothing below a command action should ever surface a raw stack trace to an
|
|
119
|
+
* agent parsing stdout. Uncaught failures exit 70 with the same JSON envelope
|
|
120
|
+
* shape as every other error.
|
|
121
|
+
*/
|
|
122
|
+
function reportInternalError(err: unknown): void {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
if (err instanceof TelemetryReadError) {
|
|
125
|
+
// A log that exists but cannot be read is an I/O failure, not an empty log.
|
|
126
|
+
// Returning `[]` here would report a broken telemetry store as a healthy one.
|
|
127
|
+
finish(
|
|
128
|
+
{
|
|
129
|
+
success: false,
|
|
130
|
+
errorCode: ErrorCode.READ_FAILED,
|
|
131
|
+
path: err.file,
|
|
132
|
+
message,
|
|
133
|
+
recovery: "Check that the telemetry log is readable, or run `hashpilot telemetry clear`.",
|
|
134
|
+
},
|
|
135
|
+
ExitCode.IO,
|
|
136
|
+
);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (err instanceof PathDeniedError) {
|
|
140
|
+
finish({ success: false, errorCode: err.errorCode, path: err.path, message }, ExitCode.USAGE);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// Commander's async actions reject outside the try/catch around `parse()`,
|
|
144
|
+
// so a plain missing file lands here. Those are ordinary I/O failures, not
|
|
145
|
+
// HashPilot bugs — reporting them as exit 70 tells an agent to file a bug
|
|
146
|
+
// report instead of fixing its path.
|
|
147
|
+
const syscall = (err as { code?: string } | undefined)?.code;
|
|
148
|
+
if (syscall !== undefined && IO_SYSCALL_CODES.has(syscall)) {
|
|
149
|
+
finish(
|
|
150
|
+
{
|
|
151
|
+
success: false,
|
|
152
|
+
errorCode: syscall === "ENOENT" ? ErrorCode.FILE_NOT_FOUND : ErrorCode.WRITE_FAILED,
|
|
153
|
+
message,
|
|
154
|
+
recovery: "Check that the path exists and is readable and writable.",
|
|
155
|
+
},
|
|
156
|
+
ExitCode.IO,
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
finish(
|
|
161
|
+
{
|
|
162
|
+
success: false,
|
|
163
|
+
errorCode: "INTERNAL_ERROR",
|
|
164
|
+
message,
|
|
165
|
+
detail: err instanceof Error ? err.stack : undefined,
|
|
166
|
+
recovery: "This is a bug in HashPilot. Please report it with the command that triggered it.",
|
|
167
|
+
},
|
|
168
|
+
ExitCode.INTERNAL,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
process.on("uncaughtException", reportInternalError);
|
|
173
|
+
process.on("unhandledRejection", reportInternalError);
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Route Commander's own parse failures (unknown flag, missing required
|
|
177
|
+
* argument, bad choice) through the JSON usage envelope instead of letting a
|
|
178
|
+
* bare `error: unknown option '--x'` line escape to stderr. An agent that
|
|
179
|
+
* cannot parse the failure cannot self-correct from it (#57).
|
|
180
|
+
*
|
|
181
|
+
* `--help` and `--version` also arrive here as CommanderErrors; those already
|
|
182
|
+
* wrote their output to stdout and must exit with their own code.
|
|
183
|
+
*/
|
|
184
|
+
function applyExitOverride(cmd: Command): void {
|
|
185
|
+
cmd.exitOverride();
|
|
186
|
+
cmd.configureOutput({ writeErr: () => {} });
|
|
187
|
+
for (const sub of cmd.commands) applyExitOverride(sub as Command);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
applyExitOverride(program);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
program.parse();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err instanceof CommanderError) {
|
|
196
|
+
if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") {
|
|
197
|
+
process.exit(err.exitCode);
|
|
198
|
+
}
|
|
199
|
+
const attempted = process.argv[2];
|
|
200
|
+
if (attempted && program.commands.some((c) => (c as Command).name() === attempted)) {
|
|
201
|
+
setCommand(attempted);
|
|
202
|
+
}
|
|
203
|
+
usageError(err.message.replace(/^error: /, ""), {
|
|
204
|
+
recovery: `Run \`hashpilot ${attempted && !attempted.startsWith("-") ? attempted + " " : ""}--help\` for the accepted arguments.`,
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
reportInternalError(err);
|
|
208
|
+
}
|
|
209
|
+
}
|