@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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +777 -0
  3. package/docs/ADAPTER-CONTRACT.md +1260 -0
  4. package/docs/ARCHITECTURE.md +846 -0
  5. package/docs/CLI-QUICKREF.md +827 -0
  6. package/docs/COMPETITIVE-ANALYSIS.md +307 -0
  7. package/docs/INSTALL.md +403 -0
  8. package/docs/INTEGRATION-CLAUDE.md +126 -0
  9. package/docs/INTEGRATION-MCP.md +196 -0
  10. package/docs/INTEGRATION-OPENCODE.md +136 -0
  11. package/docs/INTEGRATION-PI.md +195 -0
  12. package/package.json +77 -0
  13. package/scripts/build-site.sh +39 -0
  14. package/scripts/doctor.sh +218 -0
  15. package/scripts/gen-cli-quickref.ts +232 -0
  16. package/scripts/install-cli.sh +60 -0
  17. package/scripts/install.sh +466 -0
  18. package/scripts/roadmap-lint.ts +200 -0
  19. package/scripts/uninstall.sh +202 -0
  20. package/src/cli-node.cjs +51 -0
  21. package/src/cli.ts +209 -0
  22. package/src/commands/ast.ts +255 -0
  23. package/src/commands/diff.ts +98 -0
  24. package/src/commands/edit.ts +93 -0
  25. package/src/commands/hash.ts +64 -0
  26. package/src/commands/intent.ts +68 -0
  27. package/src/commands/maintenance.ts +191 -0
  28. package/src/commands/mcp.ts +28 -0
  29. package/src/commands/provenance.ts +111 -0
  30. package/src/commands/read.ts +117 -0
  31. package/src/commands/route.ts +42 -0
  32. package/src/commands/shared.ts +65 -0
  33. package/src/commands/telemetry.ts +126 -0
  34. package/src/commands/verify.ts +61 -0
  35. package/src/core/ast-edit.ts +2357 -0
  36. package/src/core/batch-edit.ts +185 -0
  37. package/src/core/config.ts +189 -0
  38. package/src/core/diff-engine.ts +474 -0
  39. package/src/core/doctor.ts +303 -0
  40. package/src/core/encoding.ts +116 -0
  41. package/src/core/envelope.ts +163 -0
  42. package/src/core/exit-codes.ts +198 -0
  43. package/src/core/format.ts +339 -0
  44. package/src/core/grep.ts +180 -0
  45. package/src/core/hash-edit.ts +416 -0
  46. package/src/core/index.ts +155 -0
  47. package/src/core/intent.ts +584 -0
  48. package/src/core/locking.ts +292 -0
  49. package/src/core/module-system.ts +142 -0
  50. package/src/core/operations.ts +557 -0
  51. package/src/core/output.ts +122 -0
  52. package/src/core/path-normalize.ts +61 -0
  53. package/src/core/paths.ts +326 -0
  54. package/src/core/plan-executor.ts +437 -0
  55. package/src/core/platform.ts +132 -0
  56. package/src/core/provenance.ts +214 -0
  57. package/src/core/read.ts +111 -0
  58. package/src/core/redact.ts +98 -0
  59. package/src/core/resolve-content.ts +12 -0
  60. package/src/core/router.ts +463 -0
  61. package/src/core/snapshot.ts +346 -0
  62. package/src/core/telemetry.ts +838 -0
  63. package/src/core/utils.ts +7 -0
  64. package/src/core/verify-baseline.ts +186 -0
  65. package/src/core/verify-scope.ts +282 -0
  66. package/src/core/verify.ts +753 -0
  67. package/src/mcp/server.ts +325 -0
  68. package/templates/claude-section.md +12 -0
  69. package/templates/opencode-agent.md +106 -0
  70. package/templates/opencode-skill.md +241 -0
  71. package/templates/pi-extension.ts +288 -0
  72. package/templates/pi-skill.md +123 -0
  73. 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