@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,218 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# HashPilot Doctor — Standalone installation health check
|
|
3
|
+
# Can run even when CLI is not on PATH.
|
|
4
|
+
|
|
5
|
+
# Derive the version from package.json rather than hardcoding a stale literal
|
|
6
|
+
# here (#156). This script is standalone (runs before the CLI is installed),
|
|
7
|
+
# so it reads package.json directly instead of importing any TS module.
|
|
8
|
+
# Resolve the script's real directory (not just dirname "$0") so this still
|
|
9
|
+
# works if invoked via a relative path from another cwd or through a symlink —
|
|
10
|
+
# the same lesson #157/#158 learned the hard way for install.sh. This script
|
|
11
|
+
# is documented to run from a local checkout (`bash scripts/doctor.sh`), not
|
|
12
|
+
# curl-piped, so $0 is always a real path here and this resolution is safe.
|
|
13
|
+
HASHPILOT_SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd 2>/dev/null)"
|
|
14
|
+
HASHPILOT_VERSION="$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "${HASHPILOT_SCRIPT_DIR:-.}/../package.json" 2>/dev/null | head -1)"
|
|
15
|
+
HASHPILOT_VERSION="${HASHPILOT_VERSION:-unknown}"
|
|
16
|
+
# shellcheck disable=SC2034
|
|
17
|
+
BOLD='\033[1m'
|
|
18
|
+
DIM='\033[2m'
|
|
19
|
+
GREEN='\033[0;32m'
|
|
20
|
+
YELLOW='\033[0;33m'
|
|
21
|
+
RED='\033[0;31m'
|
|
22
|
+
NC='\033[0m'
|
|
23
|
+
|
|
24
|
+
PASS=0
|
|
25
|
+
FAIL=0
|
|
26
|
+
WARN=0
|
|
27
|
+
SKIP=0
|
|
28
|
+
RESULTS=()
|
|
29
|
+
|
|
30
|
+
pass() { PASS=$((PASS+1)); RESULTS+=("{\"name\":\"$1\",\"status\":\"pass\",\"message\":\"$2\"}"); }
|
|
31
|
+
fail() { FAIL=$((FAIL+1)); RESULTS+=("{\"name\":\"$1\",\"status\":\"fail\",\"message\":\"$2\"}"); }
|
|
32
|
+
warn() { WARN=$((WARN+1)); RESULTS+=("{\"name\":\"$1\",\"status\":\"warn\",\"message\":\"$2\"}"); }
|
|
33
|
+
skip() { SKIP=$((SKIP+1)); RESULTS+=("{\"name\":\"$1\",\"status\":\"skip\",\"message\":\"$2\"}"); }
|
|
34
|
+
|
|
35
|
+
TARGET_DIR="${HASHPILOT_DIR:-${HOME}/.agentic-tools}"
|
|
36
|
+
MANIFEST="$TARGET_DIR/manifest.json"
|
|
37
|
+
|
|
38
|
+
echo "${BOLD}HashPilot Doctor v${HASHPILOT_VERSION}${NC}"
|
|
39
|
+
echo ""
|
|
40
|
+
|
|
41
|
+
# 1. Core directory
|
|
42
|
+
if [[ -d "$TARGET_DIR/structured-editing" ]]; then
|
|
43
|
+
pass "core-directory" "Found: $TARGET_DIR/structured-editing"
|
|
44
|
+
else
|
|
45
|
+
fail "core-directory" "Missing: $TARGET_DIR/structured-editing"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
# 2. Core source files
|
|
49
|
+
if [[ -f "$TARGET_DIR/structured-editing/src/cli.ts" ]]; then
|
|
50
|
+
pass "core-cli.ts" "Found: $TARGET_DIR/structured-editing/src/cli.ts"
|
|
51
|
+
else
|
|
52
|
+
fail "core-cli.ts" "Missing: $TARGET_DIR/structured-editing/src/cli.ts"
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [[ -f "$TARGET_DIR/structured-editing/package.json" ]]; then
|
|
56
|
+
pass "core-package.json" "Found: $TARGET_DIR/structured-editing/package.json"
|
|
57
|
+
else
|
|
58
|
+
fail "core-package.json" "Missing: $TARGET_DIR/structured-editing/package.json"
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
# 3. Dependencies
|
|
62
|
+
if [[ -d "$TARGET_DIR/structured-editing/node_modules" ]]; then
|
|
63
|
+
pass "core-dependencies" "node_modules present"
|
|
64
|
+
else
|
|
65
|
+
fail "core-dependencies" "node_modules missing — run 'bun install' in $TARGET_DIR/structured-editing"
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
# 4. CLI launcher
|
|
69
|
+
if [[ -x "$TARGET_DIR/bin/hashpilot" ]]; then
|
|
70
|
+
pass "cli-launcher" "Found: $TARGET_DIR/bin/hashpilot"
|
|
71
|
+
else
|
|
72
|
+
fail "cli-launcher" "Missing: $TARGET_DIR/bin/hashpilot"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# 5. CLI on PATH
|
|
76
|
+
if command -v hashpilot &>/dev/null; then
|
|
77
|
+
CLI_PATH=$(command -v hashpilot)
|
|
78
|
+
pass "cli-on-path" "Found at: $CLI_PATH"
|
|
79
|
+
else
|
|
80
|
+
warn "cli-on-path" "hashpilot not on PATH — add $TARGET_DIR/bin to PATH"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# 6. CLI executable
|
|
84
|
+
if command -v hashpilot &>/dev/null; then
|
|
85
|
+
VER=$(hashpilot --version 2>/dev/null || echo "error")
|
|
86
|
+
if [[ "$VER" != "error" ]]; then
|
|
87
|
+
pass "cli-executable" "CLI works: $VER"
|
|
88
|
+
else
|
|
89
|
+
fail "cli-executable" "CLI failed to run"
|
|
90
|
+
fi
|
|
91
|
+
elif [[ -x "$TARGET_DIR/bin/hashpilot" ]]; then
|
|
92
|
+
VER=$("$TARGET_DIR/bin/hashpilot" --version 2>/dev/null || echo "error")
|
|
93
|
+
if [[ "$VER" != "error" ]]; then
|
|
94
|
+
pass "cli-executable" "CLI works: $VER"
|
|
95
|
+
else
|
|
96
|
+
fail "cli-executable" "CLI failed to run (try: bun install in $TARGET_DIR/structured-editing)"
|
|
97
|
+
fi
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# 7. Config file
|
|
101
|
+
if [[ -f "${HOME}/.config/hashpilot/config.json" ]]; then
|
|
102
|
+
if jq -e . "${HOME}/.config/hashpilot/config.json" >/dev/null 2>&1; then
|
|
103
|
+
pass "config-file" "Found valid config at ${HOME}/.config/hashpilot/config.json"
|
|
104
|
+
else
|
|
105
|
+
fail "config-file" "Config exists but is not valid JSON: ${HOME}/.config/hashpilot/config.json"
|
|
106
|
+
fi
|
|
107
|
+
else
|
|
108
|
+
skip "config-file" "No config file — using defaults"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# 8. Claude integration
|
|
112
|
+
CLAUDE_FILE="${HOME}/.claude/CLAUDE.md"
|
|
113
|
+
CLAUDE_MARKER="HashPilot Claude"
|
|
114
|
+
if [[ -f "$CLAUDE_FILE" ]]; then
|
|
115
|
+
if grep -q "$CLAUDE_MARKER" "$CLAUDE_FILE" 2>/dev/null; then
|
|
116
|
+
pass "claude-integration" "HashPilot section found in $CLAUDE_FILE"
|
|
117
|
+
else
|
|
118
|
+
warn "claude-integration" "CLAUDE.md exists but HashPilot section missing"
|
|
119
|
+
fi
|
|
120
|
+
else
|
|
121
|
+
skip "claude-integration" "Claude CLAUDE.md not found"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# 9. OpenCode integration
|
|
125
|
+
if [[ -f "${HOME}/.config/opencode/skills/hashpilot/SKILL.md" ]]; then
|
|
126
|
+
pass "opencode-skill" "Found OpenCode skill"
|
|
127
|
+
else
|
|
128
|
+
fail "opencode-skill" "Missing: ~/.config/opencode/skills/hashpilot/SKILL.md"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
if [[ -f "${HOME}/.config/opencode/agent/hashpilot.md" ]]; then
|
|
132
|
+
pass "opencode-agent" "Found OpenCode agent"
|
|
133
|
+
else
|
|
134
|
+
fail "opencode-agent" "Missing: ~/.config/opencode/agent/hashpilot.md"
|
|
135
|
+
fi
|
|
136
|
+
|
|
137
|
+
# 10. Pi integration
|
|
138
|
+
if [[ -f "${HOME}/.pi/agent/extensions/hashpilot.ts" ]]; then
|
|
139
|
+
pass "pi-extension" "Found Pi extension"
|
|
140
|
+
else
|
|
141
|
+
fail "pi-extension" "Missing: ~/.pi/agent/extensions/hashpilot.ts"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
if [[ -f "${HOME}/.pi/agent/skills/hashpilot/SKILL.md" ]]; then
|
|
145
|
+
pass "pi-skill" "Found Pi skill"
|
|
146
|
+
else
|
|
147
|
+
fail "pi-skill" "Missing: ~/.pi/agent/skills/hashpilot/SKILL.md"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# 11. Telemetry writable
|
|
151
|
+
if mkdir -p "$TARGET_DIR/logs" 2>/dev/null; then
|
|
152
|
+
TESTFILE="$TARGET_DIR/logs/.doctor-write-test-$$"
|
|
153
|
+
if echo "ok" > "$TESTFILE" 2>/dev/null; then
|
|
154
|
+
rm -f "$TESTFILE"
|
|
155
|
+
pass "telemetry-writable" "Log dir is writable: $TARGET_DIR/logs"
|
|
156
|
+
else
|
|
157
|
+
fail "telemetry-writable" "Log dir not writable: $TARGET_DIR/logs"
|
|
158
|
+
fi
|
|
159
|
+
else
|
|
160
|
+
fail "telemetry-writable" "Cannot create log dir: $TARGET_DIR/logs"
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
# 12. Manifest
|
|
164
|
+
if [[ -f "$MANIFEST" ]]; then
|
|
165
|
+
pass "manifest" "Found: $MANIFEST"
|
|
166
|
+
else
|
|
167
|
+
fail "manifest" "Missing: $MANIFEST (rerun install.sh)"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
# 13. PATH entry in shell rc
|
|
171
|
+
RC_CHECKED=false
|
|
172
|
+
for rc in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.bash_profile" "${HOME}/.profile"; do
|
|
173
|
+
if [[ -f "$rc" ]] && grep -q "hashpilot path" "$rc" 2>/dev/null; then
|
|
174
|
+
pass "path-entry" "PATH entry found in $rc"
|
|
175
|
+
RC_CHECKED=true
|
|
176
|
+
break
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
if [[ "$RC_CHECKED" != "true" ]]; then
|
|
180
|
+
warn "path-entry" "No hashpilot PATH entry found in shell rc files"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# ── Summary ───────────────────────────────────────────────────────────────
|
|
184
|
+
OVERALL_HEALTHY=true
|
|
185
|
+
if [[ "$FAIL" -gt 0 ]]; then
|
|
186
|
+
OVERALL_HEALTHY=false
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
echo ""
|
|
190
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
191
|
+
if [[ "$OVERALL_HEALTHY" == "true" ]]; then
|
|
192
|
+
echo " ${GREEN}HashPilot is HEALTHY${NC}"
|
|
193
|
+
else
|
|
194
|
+
echo " ${RED}HashPilot has ISSUES${NC}"
|
|
195
|
+
fi
|
|
196
|
+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
197
|
+
printf " ${GREEN}✓${NC} Pass: %d ${RED}✗${NC} Fail: %d ${YELLOW}!${NC} Warn: %d ${DIM}·${NC} Skip: %d\n" "$PASS" "$FAIL" "$WARN" "$SKIP"
|
|
198
|
+
echo ""
|
|
199
|
+
|
|
200
|
+
# Machine-readable JSON summary on stderr (capture with 2>/dev/null to suppress)
|
|
201
|
+
JSON_RESULT=$(cat <<EOF
|
|
202
|
+
{
|
|
203
|
+
"version": "${HASHPILOT_VERSION}",
|
|
204
|
+
"healthy": ${OVERALL_HEALTHY},
|
|
205
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
206
|
+
"summary": { "pass": ${PASS}, "fail": ${FAIL}, "warn": ${WARN}, "skip": ${SKIP} },
|
|
207
|
+
"checks": [
|
|
208
|
+
$(IFS=,; echo "${RESULTS[*]}")
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
EOF
|
|
212
|
+
)
|
|
213
|
+
echo "$JSON_RESULT" >&2
|
|
214
|
+
|
|
215
|
+
if [[ "$OVERALL_HEALTHY" != "true" ]]; then
|
|
216
|
+
exit 1
|
|
217
|
+
fi
|
|
218
|
+
exit 0
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Generates the command table in docs/CLI-QUICKREF.md by walking the CLI's own
|
|
4
|
+
* `--help` output. Reading the real help (rather than importing the Commander
|
|
5
|
+
* program) means the reference can never describe a command shape the shipped
|
|
6
|
+
* binary does not actually accept — src/cli.ts calls `program.parse()` at module
|
|
7
|
+
* load, so it cannot be imported without executing it.
|
|
8
|
+
*
|
|
9
|
+
* bun run scripts/gen-cli-quickref.ts # rewrite the generated block
|
|
10
|
+
* bun run scripts/gen-cli-quickref.ts --check # exit 1 if the file is stale
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { spawnSync } from "node:child_process";
|
|
15
|
+
|
|
16
|
+
export const BEGIN_MARKER = "<!-- BEGIN GENERATED: command reference -->";
|
|
17
|
+
export const END_MARKER = "<!-- END GENERATED: command reference -->";
|
|
18
|
+
|
|
19
|
+
const ROOT = join(import.meta.dir, "..");
|
|
20
|
+
const DOC_PATH = join(ROOT, "docs", "CLI-QUICKREF.md");
|
|
21
|
+
const CLI = join(ROOT, "src", "cli.ts");
|
|
22
|
+
|
|
23
|
+
export interface CommandDoc {
|
|
24
|
+
/** Full subcommand path, e.g. `["ast", "rename-symbol"]`. */
|
|
25
|
+
path: string[];
|
|
26
|
+
description: string;
|
|
27
|
+
usage: string;
|
|
28
|
+
args: Array<{ name: string; description: string }>;
|
|
29
|
+
options: Array<{ flags: string; description: string }>;
|
|
30
|
+
/** Present only on group commands like `ast` or `telemetry`. */
|
|
31
|
+
children: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function help(path: string[]): string {
|
|
35
|
+
const label = path.join(" ") || "(root)";
|
|
36
|
+
const res = spawnSync("bun", ["run", CLI, ...path, "--help"], {
|
|
37
|
+
encoding: "utf8",
|
|
38
|
+
cwd: ROOT,
|
|
39
|
+
env: { ...process.env, HASHPILOT_TELEMETRY: "0" },
|
|
40
|
+
timeout: 30_000,
|
|
41
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
42
|
+
});
|
|
43
|
+
// Anything short of a clean exit means the captured stdout may be a partial
|
|
44
|
+
// help page, and a partial page generates plausible-looking but wrong docs
|
|
45
|
+
// that still satisfy `--check`. Refuse rather than publish it.
|
|
46
|
+
if (res.error) throw new Error(`help failed for '${label}': ${res.error.message}`);
|
|
47
|
+
if (res.signal) throw new Error(`help for '${label}' was killed by ${res.signal}`);
|
|
48
|
+
if (res.status !== 0) {
|
|
49
|
+
throw new Error(`help for '${label}' exited ${res.status}: ${res.stderr?.trim()}`);
|
|
50
|
+
}
|
|
51
|
+
if (!res.stdout.includes("Usage:")) {
|
|
52
|
+
throw new Error(`help for '${label}' produced no Usage: line (truncated output?)`);
|
|
53
|
+
}
|
|
54
|
+
return res.stdout;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Splits `--help` output into its `Usage:` line and named sections. */
|
|
58
|
+
function sections(text: string): { usage: string; sections: Map<string, string[]> } {
|
|
59
|
+
const lines = text.split("\n");
|
|
60
|
+
let usage = "";
|
|
61
|
+
const out = new Map<string, string[]>();
|
|
62
|
+
let current: string | null = null;
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
if (line.startsWith("Usage:")) {
|
|
65
|
+
usage = line.slice("Usage:".length).trim();
|
|
66
|
+
current = null;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const header = /^([A-Z][A-Za-z ]*):\s*$/.exec(line);
|
|
70
|
+
if (header) {
|
|
71
|
+
current = header[1]!;
|
|
72
|
+
out.set(current, []);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (current && line.trim()) out.get(current)!.push(line);
|
|
76
|
+
}
|
|
77
|
+
return { usage, sections: out };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Splits an entry line into its term and description. Commander pads the two
|
|
82
|
+
* columns apart with at least two spaces, and a description may wrap onto
|
|
83
|
+
* continuation lines that carry no term at all.
|
|
84
|
+
*/
|
|
85
|
+
function entries(lines: string[]): Array<{ term: string; description: string }> {
|
|
86
|
+
const out: Array<{ term: string; description: string }> = [];
|
|
87
|
+
for (const raw of lines) {
|
|
88
|
+
const line = raw.trimEnd();
|
|
89
|
+
const indent = line.length - line.trimStart().length;
|
|
90
|
+
const body = line.trim();
|
|
91
|
+
if (!body) continue;
|
|
92
|
+
// A continuation line is indented past the term column and has no gap.
|
|
93
|
+
const split = /^(\S.*?)\s{2,}(.*)$/.exec(body);
|
|
94
|
+
if (!split) {
|
|
95
|
+
if (out.length && indent > 4) out[out.length - 1]!.description += ` ${body}`;
|
|
96
|
+
else out.push({ term: body, description: "" });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
out.push({ term: split[1]!, description: split[2]!.trim() });
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Recursively walks every subcommand reachable from `path`. */
|
|
105
|
+
export function walk(path: string[] = []): CommandDoc[] {
|
|
106
|
+
const parsed = sections(help(path));
|
|
107
|
+
const cmdEntries = entries(parsed.sections.get("Commands") ?? []);
|
|
108
|
+
const children = cmdEntries
|
|
109
|
+
.map((e) => e.term.split(/\s+/)[0]!)
|
|
110
|
+
.filter((name) => name !== "help");
|
|
111
|
+
|
|
112
|
+
const doc: CommandDoc = {
|
|
113
|
+
path,
|
|
114
|
+
description: "",
|
|
115
|
+
usage: parsed.usage,
|
|
116
|
+
args: entries(parsed.sections.get("Arguments") ?? []).map((e) => ({
|
|
117
|
+
name: e.term,
|
|
118
|
+
description: e.description,
|
|
119
|
+
})),
|
|
120
|
+
options: entries(parsed.sections.get("Options") ?? [])
|
|
121
|
+
.map((e) => ({ flags: e.term, description: e.description }))
|
|
122
|
+
.filter((o) => !/^-h, --help/.test(o.flags)),
|
|
123
|
+
children,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// The root document carries the global flags (`--allowed-root`,
|
|
127
|
+
// `--allow-outside-root`, `--no-telemetry`, `--version`), so it is kept, not
|
|
128
|
+
// dropped — those are exactly the flags an agent must not have to guess at.
|
|
129
|
+
const results: CommandDoc[] = [doc];
|
|
130
|
+
// The root's own description lives above `Usage:`; subcommand descriptions
|
|
131
|
+
// come from the parent's Commands table, so backfill them during recursion.
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
const sub = walk([...path, child]);
|
|
134
|
+
const own = sub.find((s) => s.path.join(" ") === [...path, child].join(" "));
|
|
135
|
+
if (own) {
|
|
136
|
+
own.description =
|
|
137
|
+
cmdEntries.find((e) => e.term.split(/\s+/)[0] === child)?.description ?? "";
|
|
138
|
+
}
|
|
139
|
+
results.push(...sub);
|
|
140
|
+
}
|
|
141
|
+
return results;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cell(text: string): string {
|
|
145
|
+
return text.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function render(docs: CommandDoc[]): string {
|
|
149
|
+
const root = docs.find((d) => d.path.length === 0);
|
|
150
|
+
const leaves = docs.filter((d) => d.path.length > 0 && d.children.length === 0);
|
|
151
|
+
const groups = docs.filter((d) => d.path.length > 0 && d.children.length > 0);
|
|
152
|
+
|
|
153
|
+
const out: string[] = [];
|
|
154
|
+
out.push(`_${leaves.length} commands, generated from \`--help\`. Do not edit by hand — run \`bun run gen:cli-quickref\`._`);
|
|
155
|
+
out.push("");
|
|
156
|
+
if (root) {
|
|
157
|
+
out.push("### Global options");
|
|
158
|
+
out.push("");
|
|
159
|
+
out.push("Accepted before the subcommand, e.g. `hashpilot --allowed-root /srv/app read-many f.ts`.");
|
|
160
|
+
out.push("");
|
|
161
|
+
out.push("```");
|
|
162
|
+
out.push(root.usage);
|
|
163
|
+
out.push("```");
|
|
164
|
+
out.push("");
|
|
165
|
+
out.push("| Flag | Meaning |");
|
|
166
|
+
out.push("|------|---------|");
|
|
167
|
+
for (const o of root.options) out.push(`| \`${cell(o.flags)}\` | ${cell(o.description)} |`);
|
|
168
|
+
out.push("");
|
|
169
|
+
}
|
|
170
|
+
out.push("### Command groups");
|
|
171
|
+
out.push("");
|
|
172
|
+
out.push("| Group | Subcommands |");
|
|
173
|
+
out.push("|-------|-------------|");
|
|
174
|
+
for (const g of groups) {
|
|
175
|
+
out.push(`| \`${g.path.join(" ")}\` | ${g.children.map((c) => `\`${c}\``).join(", ")} |`);
|
|
176
|
+
}
|
|
177
|
+
out.push("");
|
|
178
|
+
out.push("### Commands");
|
|
179
|
+
out.push("");
|
|
180
|
+
|
|
181
|
+
for (const d of leaves) {
|
|
182
|
+
out.push(`#### \`${d.path.join(" ")}\``);
|
|
183
|
+
out.push("");
|
|
184
|
+
if (d.description) out.push(d.description);
|
|
185
|
+
out.push("");
|
|
186
|
+
out.push("```");
|
|
187
|
+
out.push(d.usage);
|
|
188
|
+
out.push("```");
|
|
189
|
+
out.push("");
|
|
190
|
+
if (d.args.length) {
|
|
191
|
+
out.push("| Positional | Meaning |");
|
|
192
|
+
out.push("|------------|---------|");
|
|
193
|
+
for (const a of d.args) out.push(`| \`${cell(a.name)}\` | ${cell(a.description)} |`);
|
|
194
|
+
out.push("");
|
|
195
|
+
}
|
|
196
|
+
if (d.options.length) {
|
|
197
|
+
out.push("| Flag | Meaning |");
|
|
198
|
+
out.push("|------|---------|");
|
|
199
|
+
for (const o of d.options) out.push(`| \`${cell(o.flags)}\` | ${cell(o.description)} |`);
|
|
200
|
+
out.push("");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out.join("\n").trimEnd();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Replaces the generated block in `doc`, leaving hand-written prose intact. */
|
|
207
|
+
export function spliceGenerated(doc: string, generated: string): string {
|
|
208
|
+
const begin = doc.indexOf(BEGIN_MARKER);
|
|
209
|
+
const end = doc.indexOf(END_MARKER);
|
|
210
|
+
if (begin === -1 || end === -1 || end < begin) {
|
|
211
|
+
throw new Error(`docs/CLI-QUICKREF.md is missing the generated-block markers`);
|
|
212
|
+
}
|
|
213
|
+
return (
|
|
214
|
+
doc.slice(0, begin + BEGIN_MARKER.length) + "\n\n" + generated + "\n\n" + doc.slice(end)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (import.meta.main) {
|
|
219
|
+
const check = process.argv.includes("--check");
|
|
220
|
+
const current = readFileSync(DOC_PATH, "utf8");
|
|
221
|
+
const next = spliceGenerated(current, render(walk()));
|
|
222
|
+
if (current === next) {
|
|
223
|
+
console.log("✓ docs/CLI-QUICKREF.md is up to date");
|
|
224
|
+
process.exit(0);
|
|
225
|
+
}
|
|
226
|
+
if (check) {
|
|
227
|
+
console.error("✗ docs/CLI-QUICKREF.md is stale. Run: bun run gen:cli-quickref");
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
writeFileSync(DOC_PATH, next);
|
|
231
|
+
console.log("✓ regenerated docs/CLI-QUICKREF.md");
|
|
232
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Dev install: symlink the CLI launcher into ~/.agentic-tools/bin and make sure
|
|
3
|
+
# that directory is actually on PATH.
|
|
4
|
+
#
|
|
5
|
+
# The symlink alone is not an install. Before this script, `bun run install-cli`
|
|
6
|
+
# created the link and stopped, so `hashpilot` was not a runnable command in a
|
|
7
|
+
# fresh shell and `doctor` reported a broken installation. PATH wiring lives in
|
|
8
|
+
# scripts/install.sh for the full install; this is the same block, so the two
|
|
9
|
+
# paths converge on one marker and `scripts/uninstall.sh` removes either.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
BIN_DIR="${HOME}/.agentic-tools/bin"
|
|
13
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
14
|
+
LAUNCHER="${REPO_ROOT}/src/cli-node.cjs"
|
|
15
|
+
|
|
16
|
+
mkdir -p "$BIN_DIR"
|
|
17
|
+
ln -sf "$LAUNCHER" "$BIN_DIR/hashpilot"
|
|
18
|
+
echo "Linked $BIN_DIR/hashpilot -> $LAUNCHER"
|
|
19
|
+
|
|
20
|
+
# Stale symlink from the pre-3.1 binary name; leaving it makes `doctor` and the
|
|
21
|
+
# manifest disagree about what is installed.
|
|
22
|
+
if [ -L "$BIN_DIR/structured-edit" ]; then
|
|
23
|
+
rm -f "$BIN_DIR/structured-edit"
|
|
24
|
+
echo "Removed stale symlink: $BIN_DIR/structured-edit"
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
detect_rc() {
|
|
28
|
+
if [ -n "${HASHPILOT_SHELL_RC:-}" ]; then echo "$HASHPILOT_SHELL_RC"; return; fi
|
|
29
|
+
case "${SHELL:-}" in
|
|
30
|
+
*/zsh) echo "${HOME}/.zshrc"; return ;;
|
|
31
|
+
*/bash) [ -f "${HOME}/.bashrc" ] && { echo "${HOME}/.bashrc"; return; } ;;
|
|
32
|
+
esac
|
|
33
|
+
for f in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.bash_profile" "${HOME}/.profile"; do
|
|
34
|
+
if [ -f "$f" ]; then echo "$f"; return; fi
|
|
35
|
+
done
|
|
36
|
+
echo "${HOME}/.bashrc"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
RC_FILE=$(detect_rc)
|
|
40
|
+
PATH_MARKER_START="# >>> hashpilot path >>>"
|
|
41
|
+
PATH_MARKER_END="# <<< hashpilot path <<<"
|
|
42
|
+
PATH_LINE="export PATH=\"\$HOME/.agentic-tools/bin:\$PATH\""
|
|
43
|
+
|
|
44
|
+
if grep -q "$PATH_MARKER_START" "$RC_FILE" 2>/dev/null; then
|
|
45
|
+
echo "PATH entry already present in $RC_FILE"
|
|
46
|
+
else
|
|
47
|
+
{
|
|
48
|
+
echo ""
|
|
49
|
+
echo "$PATH_MARKER_START"
|
|
50
|
+
echo "$PATH_LINE"
|
|
51
|
+
echo "$PATH_MARKER_END"
|
|
52
|
+
} >> "$RC_FILE"
|
|
53
|
+
echo "Added PATH entry to $RC_FILE"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
case ":${PATH}:" in
|
|
57
|
+
*":${BIN_DIR}:"*) echo "hashpilot is on PATH in this shell." ;;
|
|
58
|
+
*) echo "Run this to use hashpilot in the current shell:"
|
|
59
|
+
echo " export PATH=\"\$HOME/.agentic-tools/bin:\$PATH\"" ;;
|
|
60
|
+
esac
|