harnery 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/completion.d.ts.map +1 -1
- package/dist/commands/completion.js +48 -10
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +10 -2
- package/dist/core/hooks/cli.js +4 -10
- package/dist/core/hooks/guard-path.d.ts +29 -0
- package/dist/core/hooks/guard-path.d.ts.map +1 -0
- package/dist/core/hooks/guard-path.js +38 -0
- package/dist/lib/completion/bash.d.ts +15 -0
- package/dist/lib/completion/bash.d.ts.map +1 -1
- package/dist/lib/completion/bash.js +35 -0
- package/dist/lib/completion/fish.d.ts +10 -0
- package/dist/lib/completion/fish.d.ts.map +1 -1
- package/dist/lib/completion/fish.js +21 -0
- package/dist/lib/completion/index.d.ts +4 -3
- package/dist/lib/completion/index.d.ts.map +1 -1
- package/dist/lib/completion/index.js +4 -3
- package/dist/lib/completion/resolve.d.ts +52 -0
- package/dist/lib/completion/resolve.d.ts.map +1 -0
- package/dist/lib/completion/resolve.js +171 -0
- package/dist/lib/completion/zsh.d.ts +8 -0
- package/dist/lib/completion/zsh.d.ts.map +1 -1
- package/dist/lib/completion/zsh.js +33 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +6 -0
- package/package.json +1 -1
- package/src/commands/completion.ts +62 -9
- package/src/core/agents/rules/claim-conflict.ts +10 -2
- package/src/core/hooks/cli.ts +4 -8
- package/src/core/hooks/guard-path.ts +34 -0
- package/src/lib/completion/bash.ts +36 -0
- package/src/lib/completion/fish.ts +22 -0
- package/src/lib/completion/index.ts +12 -3
- package/src/lib/completion/resolve.ts +210 -0
- package/src/lib/completion/zsh.ts +34 -0
- package/src/lib/docs-lint.ts +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/commands/completion.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../../src/commands/completion.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAkB1E;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,WAAW,EAClB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,IAAI,CAqGN"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
-
import { generateBash, generateFish, generateZsh, walkProgram, } from "../lib/completion/index.js";
|
|
3
|
+
import { encodeResult, generateBash, generateBashDynamic, generateFish, generateFishDynamic, generateZsh, generateZshDynamic, resolveCompletions, walkProgram, } from "../lib/completion/index.js";
|
|
4
4
|
const noopProviderRunner = async () => [];
|
|
5
5
|
const noopLookup = () => undefined;
|
|
6
6
|
/**
|
|
@@ -22,25 +22,35 @@ export function registerCompletionCommand(program, _emit, context) {
|
|
|
22
22
|
const root = program
|
|
23
23
|
.command("completion")
|
|
24
24
|
.description("Shell tab-completion. Emit a script per shell or install to the standard location.");
|
|
25
|
+
const dynamicHint = "Emit a thin shim that calls the binary at tab-time (never goes stale; install once)";
|
|
25
26
|
root
|
|
26
27
|
.command("bash")
|
|
27
28
|
.description("Emit bash completion script to stdout")
|
|
28
|
-
.
|
|
29
|
-
|
|
29
|
+
.option("--dynamic", dynamicHint)
|
|
30
|
+
.action((opts) => {
|
|
31
|
+
const out = opts.dynamic
|
|
32
|
+
? generateBashDynamic(program.name())
|
|
33
|
+
: generateBash(walkProgram(program, lookup), program.name());
|
|
30
34
|
process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
|
|
31
35
|
});
|
|
32
36
|
root
|
|
33
37
|
.command("zsh")
|
|
34
38
|
.description("Emit zsh completion script to stdout")
|
|
35
|
-
.
|
|
36
|
-
|
|
39
|
+
.option("--dynamic", dynamicHint)
|
|
40
|
+
.action((opts) => {
|
|
41
|
+
const out = opts.dynamic
|
|
42
|
+
? generateZshDynamic(program.name())
|
|
43
|
+
: generateZsh(walkProgram(program, lookup), program.name());
|
|
37
44
|
process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
|
|
38
45
|
});
|
|
39
46
|
root
|
|
40
47
|
.command("fish")
|
|
41
48
|
.description("Emit fish completion script to stdout")
|
|
42
|
-
.
|
|
43
|
-
|
|
49
|
+
.option("--dynamic", dynamicHint)
|
|
50
|
+
.action((opts) => {
|
|
51
|
+
const out = opts.dynamic
|
|
52
|
+
? generateFishDynamic(program.name())
|
|
53
|
+
: generateFish(walkProgram(program, lookup), program.name());
|
|
44
54
|
process.stdout.write(out); // raw bytes: shell completion scripts must be unframed (consumer evals stdout).
|
|
45
55
|
});
|
|
46
56
|
root
|
|
@@ -49,6 +59,7 @@ export function registerCompletionCommand(program, _emit, context) {
|
|
|
49
59
|
.option("--shell <name>", "bash | zsh | fish (default: auto-detect from $SHELL)")
|
|
50
60
|
.option("--path <file>", "Override destination path")
|
|
51
61
|
.option("--print-path", "Print the destination path and exit (no write)")
|
|
62
|
+
.option("--dynamic", `${dynamicHint} (recommended)`)
|
|
52
63
|
.action(async (opts) => {
|
|
53
64
|
await installCompletion(program, opts, lookup);
|
|
54
65
|
});
|
|
@@ -72,6 +83,26 @@ export function registerCompletionCommand(program, _emit, context) {
|
|
|
72
83
|
});
|
|
73
84
|
// Keep TS happy that the variable is used.
|
|
74
85
|
void hidden;
|
|
86
|
+
// Hidden internal entry for DYNAMIC completion: the thin shim passes the live
|
|
87
|
+
// command line (cursor index + all words after `--`) and we compute the full
|
|
88
|
+
// candidate set from the live command tree. `--` stops option parsing so
|
|
89
|
+
// words like `-h` reach the variadic instead of being read as our flags.
|
|
90
|
+
const hiddenLine = program
|
|
91
|
+
.command("__complete-line <cword> [words...]", { hidden: true })
|
|
92
|
+
.description("Internal: full-line completion callback for the dynamic shell shim")
|
|
93
|
+
.allowUnknownOption(true)
|
|
94
|
+
.allowExcessArguments(true)
|
|
95
|
+
.action(async (cword, words) => {
|
|
96
|
+
try {
|
|
97
|
+
const result = await resolveCompletions(program, words ?? [], Number.parseInt(cword, 10) || 0, lookup, runProvider);
|
|
98
|
+
process.stdout.write(encodeResult(result)); // lint-ok-emission: shell callback; the encoded candidate/directive stream is the contract with the shim.
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Never break the user's tab: emit just the file-fallback directive.
|
|
102
|
+
process.stdout.write("\x1f:1\n"); // lint-ok-emission: shell callback fallback directive.
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
void hiddenLine;
|
|
75
106
|
}
|
|
76
107
|
async function installCompletion(program, opts, lookup) {
|
|
77
108
|
const shell = opts.shell ?? detectShell();
|
|
@@ -84,16 +115,23 @@ async function installCompletion(program, opts, lookup) {
|
|
|
84
115
|
process.stdout.write(`${destination}\n`); // lint-ok-emission: --print-path is meant to be piped (e.g., dest=$(harn completion install --print-path)).
|
|
85
116
|
return;
|
|
86
117
|
}
|
|
118
|
+
const name = program.name();
|
|
87
119
|
let content;
|
|
88
120
|
switch (shell) {
|
|
89
121
|
case "bash":
|
|
90
|
-
content =
|
|
122
|
+
content = opts.dynamic
|
|
123
|
+
? generateBashDynamic(name)
|
|
124
|
+
: generateBash(walkProgram(program, lookup), name);
|
|
91
125
|
break;
|
|
92
126
|
case "zsh":
|
|
93
|
-
content =
|
|
127
|
+
content = opts.dynamic
|
|
128
|
+
? generateZshDynamic(name)
|
|
129
|
+
: generateZsh(walkProgram(program, lookup), name);
|
|
94
130
|
break;
|
|
95
131
|
case "fish":
|
|
96
|
-
content =
|
|
132
|
+
content = opts.dynamic
|
|
133
|
+
? generateFishDynamic(name)
|
|
134
|
+
: generateFish(walkProgram(program, lookup), name);
|
|
97
135
|
break;
|
|
98
136
|
default:
|
|
99
137
|
process.stderr.write(`Unknown shell: ${shell}\n`); // lint-ok-emission: install-time error, see above.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAeF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"claim-conflict.d.ts","sourceRoot":"","sources":["../../../../src/core/agents/rules/claim-conflict.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,CAAC,GAAG,CAAC,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAeF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,YAAY,GAAG,aAAa,CAyFjF"}
|
|
@@ -62,7 +62,15 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
62
62
|
// single-agent flow can't deadlock with itself, and the rule otherwise
|
|
63
63
|
// forces release-and-reacquire cycles on every reverse-order edit pair.
|
|
64
64
|
const hasFreshPeers = otherPeers.some((p) => isFresh(p.last_heartbeat) && p.files_touched.length > 0);
|
|
65
|
-
|
|
65
|
+
// Re-editing a path already in our own files_touched acquires no new lock
|
|
66
|
+
// edge, so it can't create a circular wait — the ordering rule must not block
|
|
67
|
+
// it. Without this exemption, an agent that edits a higher-sorting file and
|
|
68
|
+
// then makes a second pass over an already-held lower-sorting file gets a
|
|
69
|
+
// spurious ordering_violation (the dominant friction source under concurrency:
|
|
70
|
+
// both agent-Gibson holding README.md and agent-Ophelia holding AGENTS.md were
|
|
71
|
+
// blocked re-editing those held files after touching a higher path, 2026-07-03).
|
|
72
|
+
const alreadyHeld = myPeer?.files_touched.includes(req.path) ?? false;
|
|
73
|
+
if (hasFreshPeers && myPeer && myPeer.files_touched.length > 0 && !alreadyHeld) {
|
|
66
74
|
// Only ACTIVE (uncommitted) edits should constrain lock ordering. A claim on
|
|
67
75
|
// a committed-clean file is a finished edit, not a held lock, so it must not
|
|
68
76
|
// wall off a lower-sorted acquisition. Without this, a long session
|
|
@@ -79,7 +87,7 @@ export function evaluateClaim(coordRoot, req) {
|
|
|
79
87
|
allow: false,
|
|
80
88
|
exit_code: 2,
|
|
81
89
|
rule: "claim.ordering_violation",
|
|
82
|
-
reason: `Cannot acquire ${req.path}
|
|
90
|
+
reason: `Cannot acquire ${req.path}: you already hold ${highest}, which sorts after it (claim ordering rule: acquire paths in sorted order to prevent deadlock between concurrent agents). Fix by editing in sorted order, or by committing ${highest} first, since a committed-clean file no longer blocks and is auto-pruned.`,
|
|
83
91
|
};
|
|
84
92
|
}
|
|
85
93
|
// Every blocker is a finished (committed-clean) edit: prune them so they
|
package/dist/core/hooks/cli.js
CHANGED
|
@@ -28,6 +28,7 @@ import { projectHeartbeats } from "../agents/state/heartbeat-projector.js";
|
|
|
28
28
|
import { shellMutationPaths } from "../agents/state/shell-mutation.js";
|
|
29
29
|
import { captureImages, detectPresence, imageJanitor, playSound, resetSoundCounters, runTurnSummary, scratchArchive, scratchJanitor, scratchRecoveryCue, soundForEvent, syncClaudeSessions, } from "./effects/index.js";
|
|
30
30
|
import { emit } from "./events/emit.js";
|
|
31
|
+
import { canonicalize } from "./guard-path.js";
|
|
31
32
|
import { detectHarness } from "./harness/detect.js";
|
|
32
33
|
import { extractBashCommand, extractToolDescription, normalizeEventName, parsePayload, } from "./harness/parse.js";
|
|
33
34
|
import { parsePsChainLine, selectAnchorPid } from "./resolve/anchor.js";
|
|
@@ -688,7 +689,9 @@ async function main() {
|
|
|
688
689
|
}
|
|
689
690
|
async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, harness) {
|
|
690
691
|
const toolName = data.tool_name ?? "";
|
|
691
|
-
const targets = collectGuardTargets(toolName, data)
|
|
692
|
+
const targets = collectGuardTargets(toolName, data)
|
|
693
|
+
.map((p) => canonicalize(coordRoot, p))
|
|
694
|
+
.filter((p) => p !== null);
|
|
692
695
|
if (targets.length === 0)
|
|
693
696
|
return;
|
|
694
697
|
const agentCoordBin = join(coordRoot, "harnery", "bin", "agent-coord");
|
|
@@ -737,15 +740,6 @@ async function runPreToolUseGuard(coordRoot, instanceId, sessionId, data, harnes
|
|
|
737
740
|
/** Canonicalize a path to monorepo-relative form. Absolute paths under
|
|
738
741
|
* coordRoot get the prefix stripped; relative paths pass through (assumed
|
|
739
742
|
* already canonical). */
|
|
740
|
-
function canonicalize(coordRoot, p) {
|
|
741
|
-
if (!p)
|
|
742
|
-
return p;
|
|
743
|
-
if (p.startsWith(`${coordRoot}/`))
|
|
744
|
-
return p.slice(coordRoot.length + 1);
|
|
745
|
-
if (p === coordRoot)
|
|
746
|
-
return ".";
|
|
747
|
-
return p;
|
|
748
|
-
}
|
|
749
743
|
/** Pull the candidate path(s) out of a write-tool payload. Empty array when
|
|
750
744
|
* the tool isn't a write or no path could be derived. */
|
|
751
745
|
function collectGuardTargets(toolName, data) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalize a write-tool target path for the claim guard.
|
|
3
|
+
*
|
|
4
|
+
* Returns the monorepo-relative path, or `null` when the target lies OUTSIDE the
|
|
5
|
+
* repo (an absolute path not under coordRoot, e.g. a `/tmp` scratchpad or other
|
|
6
|
+
* session-temp file).
|
|
7
|
+
*
|
|
8
|
+
* The claim system is intentionally repo-scoped: it coordinates monorepo files,
|
|
9
|
+
* not arbitrary absolute paths, so the guard skips out-of-repo targets. Skipping
|
|
10
|
+
* them is right on two counts. First, it keeps non-repo paths out of a
|
|
11
|
+
* heartbeat's `files_touched`. Second, the ordering rule compares raw path
|
|
12
|
+
* strings, and an absolute `/tmp/…` sorts before every repo-relative path
|
|
13
|
+
* (`/` = 0x2F < any letter), so without this a scratchpad write would spuriously
|
|
14
|
+
* "block" a legitimately-held repo file. Returning null keeps such paths out of
|
|
15
|
+
* the claim system entirely.
|
|
16
|
+
*
|
|
17
|
+
* Accepted tradeoff: this also means shared out-of-repo files (a user-level
|
|
18
|
+
* memory or plans directory) are not coordinated across agents. The alternative,
|
|
19
|
+
* normalizing every path to one consistent key so those stay coordinated, was
|
|
20
|
+
* rejected as gold-plating a rare, merge-disciplined race in a deadlock-critical
|
|
21
|
+
* path. Coordinate shared state by keeping it in the repo, not out of it.
|
|
22
|
+
*
|
|
23
|
+
* Relative inputs are assumed already-repo-relative (Codex `apply_patch` emits
|
|
24
|
+
* cwd-relative paths). The in-repo check requires the `<root>/` separator, so a
|
|
25
|
+
* sibling dir that merely shares a prefix (`/repo-other` vs `/repo`) is treated
|
|
26
|
+
* as out-of-repo, not stripped.
|
|
27
|
+
*/
|
|
28
|
+
export declare function canonicalize(coordRoot: string, p: string): string | null;
|
|
29
|
+
//# sourceMappingURL=guard-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard-path.d.ts","sourceRoot":"","sources":["../../../src/core/hooks/guard-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxE"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonicalize a write-tool target path for the claim guard.
|
|
3
|
+
*
|
|
4
|
+
* Returns the monorepo-relative path, or `null` when the target lies OUTSIDE the
|
|
5
|
+
* repo (an absolute path not under coordRoot, e.g. a `/tmp` scratchpad or other
|
|
6
|
+
* session-temp file).
|
|
7
|
+
*
|
|
8
|
+
* The claim system is intentionally repo-scoped: it coordinates monorepo files,
|
|
9
|
+
* not arbitrary absolute paths, so the guard skips out-of-repo targets. Skipping
|
|
10
|
+
* them is right on two counts. First, it keeps non-repo paths out of a
|
|
11
|
+
* heartbeat's `files_touched`. Second, the ordering rule compares raw path
|
|
12
|
+
* strings, and an absolute `/tmp/…` sorts before every repo-relative path
|
|
13
|
+
* (`/` = 0x2F < any letter), so without this a scratchpad write would spuriously
|
|
14
|
+
* "block" a legitimately-held repo file. Returning null keeps such paths out of
|
|
15
|
+
* the claim system entirely.
|
|
16
|
+
*
|
|
17
|
+
* Accepted tradeoff: this also means shared out-of-repo files (a user-level
|
|
18
|
+
* memory or plans directory) are not coordinated across agents. The alternative,
|
|
19
|
+
* normalizing every path to one consistent key so those stay coordinated, was
|
|
20
|
+
* rejected as gold-plating a rare, merge-disciplined race in a deadlock-critical
|
|
21
|
+
* path. Coordinate shared state by keeping it in the repo, not out of it.
|
|
22
|
+
*
|
|
23
|
+
* Relative inputs are assumed already-repo-relative (Codex `apply_patch` emits
|
|
24
|
+
* cwd-relative paths). The in-repo check requires the `<root>/` separator, so a
|
|
25
|
+
* sibling dir that merely shares a prefix (`/repo-other` vs `/repo`) is treated
|
|
26
|
+
* as out-of-repo, not stripped.
|
|
27
|
+
*/
|
|
28
|
+
export function canonicalize(coordRoot, p) {
|
|
29
|
+
if (!p)
|
|
30
|
+
return null;
|
|
31
|
+
if (p === coordRoot)
|
|
32
|
+
return ".";
|
|
33
|
+
if (p.startsWith(`${coordRoot}/`))
|
|
34
|
+
return p.slice(coordRoot.length + 1);
|
|
35
|
+
if (p.startsWith("/"))
|
|
36
|
+
return null; // absolute + not under coordRoot → out-of-repo
|
|
37
|
+
return p; // relative → treat as repo-relative
|
|
38
|
+
}
|
|
@@ -22,4 +22,19 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { CommandSpec } from "./walk.js";
|
|
24
24
|
export declare function generateBash(root: CommandSpec, binName: string): string;
|
|
25
|
+
/**
|
|
26
|
+
* The driver: the parts of the completion that don't depend on the command
|
|
27
|
+
* tree. Walks COMP_WORDS to determine the current command path, then dispatches
|
|
28
|
+
* to subcommand / option / value completion. `fn` is the function-name prefix
|
|
29
|
+
* and `binName` is the CLI name used for the `__complete` callback.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* DYNAMIC bash completion: a thin, tree-independent shim. Instead of baking the
|
|
33
|
+
* command tree into case-tables (which go stale on any command/flag change),
|
|
34
|
+
* it hands the live command line to `<bin> __complete-line` on every <Tab> and
|
|
35
|
+
* lets the binary — which always knows its own current tree — answer. Install
|
|
36
|
+
* once; never regenerate. The trailing `\x1f:<n>` line carries the directive
|
|
37
|
+
* bitmask (bit 0 = fall back to file completion).
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateBashDynamic(binName: string): string;
|
|
25
40
|
//# sourceMappingURL=bash.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/bash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAuBzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2FvE"}
|
|
1
|
+
{"version":3,"file":"bash.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/bash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAA8B,MAAM,WAAW,CAAC;AAuBzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CA2FvE;AAqBD;;;;;GAKG;AACH;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0B3D"}
|
|
@@ -155,6 +155,41 @@ function bashEscape(s) {
|
|
|
155
155
|
* to subcommand / option / value completion. `fn` is the function-name prefix
|
|
156
156
|
* and `binName` is the CLI name used for the `__complete` callback.
|
|
157
157
|
*/
|
|
158
|
+
/**
|
|
159
|
+
* DYNAMIC bash completion: a thin, tree-independent shim. Instead of baking the
|
|
160
|
+
* command tree into case-tables (which go stale on any command/flag change),
|
|
161
|
+
* it hands the live command line to `<bin> __complete-line` on every <Tab> and
|
|
162
|
+
* lets the binary — which always knows its own current tree — answer. Install
|
|
163
|
+
* once; never regenerate. The trailing `\x1f:<n>` line carries the directive
|
|
164
|
+
* bitmask (bit 0 = fall back to file completion).
|
|
165
|
+
*/
|
|
166
|
+
export function generateBashDynamic(binName) {
|
|
167
|
+
const fn = fnPrefix(binName);
|
|
168
|
+
return `# ${binName} bash completion (dynamic; install once, never stale). Do not edit.
|
|
169
|
+
# Source via: eval "$(${binName} completion bash --dynamic)"
|
|
170
|
+
${fn}() {
|
|
171
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
172
|
+
local raw line directive=0
|
|
173
|
+
local -a cands=()
|
|
174
|
+
raw="$(${binName} __complete-line "$COMP_CWORD" -- "\${COMP_WORDS[@]}" 2>/dev/null)" || return
|
|
175
|
+
while IFS= read -r line; do
|
|
176
|
+
[ -z "$line" ] && continue
|
|
177
|
+
if [ "\${line:0:2}" = $'\\x1f:' ]; then
|
|
178
|
+
directive="\${line:2}"
|
|
179
|
+
else
|
|
180
|
+
cands+=( "\${line%%$'\\t'*}" )
|
|
181
|
+
fi
|
|
182
|
+
done <<< "$raw"
|
|
183
|
+
if (( directive & 1 )); then
|
|
184
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
185
|
+
return
|
|
186
|
+
fi
|
|
187
|
+
local IFS=$'\\n'
|
|
188
|
+
COMPREPLY=( $(compgen -W "\${cands[*]}" -- "$cur") )
|
|
189
|
+
}
|
|
190
|
+
complete -F ${fn} ${binName}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
158
193
|
function bashDriver(fn, binName) {
|
|
159
194
|
return `${fn}() {
|
|
160
195
|
local cur prev words cword
|
|
@@ -12,5 +12,15 @@
|
|
|
12
12
|
* giving us per-tab callbacks for free.
|
|
13
13
|
*/
|
|
14
14
|
import type { CommandSpec } from "./walk.js";
|
|
15
|
+
/**
|
|
16
|
+
* DYNAMIC fish completion: a thin, tree-independent shim that calls
|
|
17
|
+
* `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
|
|
18
|
+
* the rationale). Fish renders `value\tdescription` lines natively, so the
|
|
19
|
+
* helper just strips the trailing `\x1f:<n>` directive line. Note: fish's
|
|
20
|
+
* declarative `complete -a` cannot switch to file completion from inside the
|
|
21
|
+
* helper, so the File directive is a no-op here — use static fish completion
|
|
22
|
+
* (`completion fish`) if you need file fallback on a path-valued positional.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateFishDynamic(binName: string): string;
|
|
15
25
|
export declare function generateFish(root: CommandSpec, binName: string): string;
|
|
16
26
|
//# sourceMappingURL=fish.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fish.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/fish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuD7C,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgEvE"}
|
|
1
|
+
{"version":3,"file":"fish.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/fish.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAuD7C;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAW3D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAgEvE"}
|
|
@@ -55,6 +55,27 @@ function valueAction(opt, binName) {
|
|
|
55
55
|
}
|
|
56
56
|
return { flag: "-r", arg: "" };
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* DYNAMIC fish completion: a thin, tree-independent shim that calls
|
|
60
|
+
* `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
|
|
61
|
+
* the rationale). Fish renders `value\tdescription` lines natively, so the
|
|
62
|
+
* helper just strips the trailing `\x1f:<n>` directive line. Note: fish's
|
|
63
|
+
* declarative `complete -a` cannot switch to file completion from inside the
|
|
64
|
+
* helper, so the File directive is a no-op here — use static fish completion
|
|
65
|
+
* (`completion fish`) if you need file fallback on a path-valued positional.
|
|
66
|
+
*/
|
|
67
|
+
export function generateFishDynamic(binName) {
|
|
68
|
+
const fn = `__${binName.replace(/[^a-zA-Z0-9_]/g, "_")}_complete`;
|
|
69
|
+
return `# ${binName} fish completion (dynamic; install once, never stale). Do not edit.
|
|
70
|
+
# Source via: ${binName} completion fish --dynamic | source
|
|
71
|
+
function ${fn}
|
|
72
|
+
set -l toks (commandline -opc)
|
|
73
|
+
set -l cur (commandline -ct)
|
|
74
|
+
${binName} __complete-line (count $toks) -- $toks $cur 2>/dev/null | string match -rv '^\\x1f:'
|
|
75
|
+
end
|
|
76
|
+
complete -c ${binName} -f -a '(${fn})'
|
|
77
|
+
`;
|
|
78
|
+
}
|
|
58
79
|
export function generateFish(root, binName) {
|
|
59
80
|
const out = [];
|
|
60
81
|
out.push(`# ${binName} fish completion, generated by \`${binName} completion fish\`. Do not edit.`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export { generateBash } from "./bash.js";
|
|
2
|
-
export { generateFish } from "./fish.js";
|
|
1
|
+
export { generateBash, generateBashDynamic } from "./bash.js";
|
|
2
|
+
export { generateFish, generateFishDynamic } from "./fish.js";
|
|
3
|
+
export { type Candidate, type CompletionProviderRunner, type CompletionResult, DIRECTIVE_PREFIX, Directive, encodeResult, resolveCompletions, } from "./resolve.js";
|
|
3
4
|
export { type CommandSpec, type CompletionContextLookup, type OptionSpec, type PositionalSpec, walkProgram, } from "./walk.js";
|
|
4
|
-
export { generateZsh } from "./zsh.js";
|
|
5
|
+
export { generateZsh, generateZshDynamic } from "./zsh.js";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EACL,KAAK,SAAS,EACd,KAAK,wBAAwB,EAC7B,KAAK,gBAAgB,EACrB,gBAAgB,EAChB,SAAS,EACT,YAAY,EACZ,kBAAkB,GACnB,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,WAAW,GACZ,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { generateBash } from "./bash.js";
|
|
2
|
-
export { generateFish } from "./fish.js";
|
|
1
|
+
export { generateBash, generateBashDynamic } from "./bash.js";
|
|
2
|
+
export { generateFish, generateFishDynamic } from "./fish.js";
|
|
3
|
+
export { DIRECTIVE_PREFIX, Directive, encodeResult, resolveCompletions, } from "./resolve.js";
|
|
3
4
|
export { walkProgram, } from "./walk.js";
|
|
4
|
-
export { generateZsh } from "./zsh.js";
|
|
5
|
+
export { generateZsh, generateZshDynamic } from "./zsh.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime completion resolver — the authoritative, in-process answer to
|
|
3
|
+
* "what should I suggest at this cursor position?".
|
|
4
|
+
*
|
|
5
|
+
* The static bash/zsh/fish generators bake the whole command tree into shell
|
|
6
|
+
* case-tables, which go stale the moment a command/flag is added (the script
|
|
7
|
+
* must be regenerated + reinstalled). The DYNAMIC completion path instead
|
|
8
|
+
* installs a thin, tree-independent shim that, on every <Tab>, hands the live
|
|
9
|
+
* command line back to the binary and calls THIS function. Because the binary
|
|
10
|
+
* always knows its own current command tree, a dynamic shim never goes stale —
|
|
11
|
+
* install once, ever.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors the path-walking the static bash driver does, but in one place
|
|
14
|
+
* and in TypeScript, so all three shells share identical behavior. It is
|
|
15
|
+
* deliberately decoupled from Commander internals via the CommandSpec tree
|
|
16
|
+
* (walkProgram), exactly like the static generators.
|
|
17
|
+
*/
|
|
18
|
+
import type { Command } from "commander";
|
|
19
|
+
import { type CompletionContextLookup } from "./walk.js";
|
|
20
|
+
/** Bitmask of post-processing hints for the shell shim (Cobra-style). */
|
|
21
|
+
export declare const Directive: {
|
|
22
|
+
/** Use the returned candidates as completion values. */
|
|
23
|
+
readonly Default: 0;
|
|
24
|
+
/** No candidates apply here — the shell should fall back to file completion. */
|
|
25
|
+
readonly File: 1;
|
|
26
|
+
};
|
|
27
|
+
export interface Candidate {
|
|
28
|
+
value: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface CompletionResult {
|
|
32
|
+
candidates: Candidate[];
|
|
33
|
+
directive: number;
|
|
34
|
+
}
|
|
35
|
+
/** Provider runner injected by the host CLI (same contract as `__complete`). */
|
|
36
|
+
export type CompletionProviderRunner = (provider: string, partial: string) => Promise<string[]>;
|
|
37
|
+
/**
|
|
38
|
+
* Compute completion candidates for `words` with the cursor at `cword`.
|
|
39
|
+
* `words[0]` is the bin name; `words[cword]` is the (possibly empty) token
|
|
40
|
+
* being completed. The shell shim does the final prefix-filtering against that
|
|
41
|
+
* token, so this returns the full candidate set for the slot.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveCompletions(program: Command, words: string[], cword: number, lookup: CompletionContextLookup, runProvider: CompletionProviderRunner): Promise<CompletionResult>;
|
|
44
|
+
/** Sentinel prefix for the trailing directive line in the wire protocol. */
|
|
45
|
+
export declare const DIRECTIVE_PREFIX = "\u001F:";
|
|
46
|
+
/**
|
|
47
|
+
* Serialize a result for the shell shim: one `value\tdescription` line per
|
|
48
|
+
* candidate, then a final `\x1f:<directive>` line. The \x1f (unit separator)
|
|
49
|
+
* prefix makes the directive line unambiguous against real candidate values.
|
|
50
|
+
*/
|
|
51
|
+
export declare function encodeResult(result: CompletionResult): string;
|
|
52
|
+
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../../src/lib/completion/resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAoB,KAAK,uBAAuB,EAAe,MAAM,WAAW,CAAC;AAExF,yEAAyE;AACzE,eAAO,MAAM,SAAS;IACpB,wDAAwD;;IAExD,gFAAgF;;CAExE,CAAC;AAEX,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,gFAAgF;AAChF,MAAM,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAiGhG;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EAAE,EACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,uBAAuB,EAC/B,WAAW,EAAE,wBAAwB,GACpC,OAAO,CAAC,gBAAgB,CAAC,CA4C3B;AAED,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,YAAU,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAM7D"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime completion resolver — the authoritative, in-process answer to
|
|
3
|
+
* "what should I suggest at this cursor position?".
|
|
4
|
+
*
|
|
5
|
+
* The static bash/zsh/fish generators bake the whole command tree into shell
|
|
6
|
+
* case-tables, which go stale the moment a command/flag is added (the script
|
|
7
|
+
* must be regenerated + reinstalled). The DYNAMIC completion path instead
|
|
8
|
+
* installs a thin, tree-independent shim that, on every <Tab>, hands the live
|
|
9
|
+
* command line back to the binary and calls THIS function. Because the binary
|
|
10
|
+
* always knows its own current command tree, a dynamic shim never goes stale —
|
|
11
|
+
* install once, ever.
|
|
12
|
+
*
|
|
13
|
+
* This mirrors the path-walking the static bash driver does, but in one place
|
|
14
|
+
* and in TypeScript, so all three shells share identical behavior. It is
|
|
15
|
+
* deliberately decoupled from Commander internals via the CommandSpec tree
|
|
16
|
+
* (walkProgram), exactly like the static generators.
|
|
17
|
+
*/
|
|
18
|
+
import { walkProgram } from "./walk.js";
|
|
19
|
+
/** Bitmask of post-processing hints for the shell shim (Cobra-style). */
|
|
20
|
+
export const Directive = {
|
|
21
|
+
/** Use the returned candidates as completion values. */
|
|
22
|
+
Default: 0,
|
|
23
|
+
/** No candidates apply here — the shell should fall back to file completion. */
|
|
24
|
+
File: 1,
|
|
25
|
+
};
|
|
26
|
+
function buildTables(root) {
|
|
27
|
+
const subcommandsByPath = new Map();
|
|
28
|
+
const optionsByPath = new Map();
|
|
29
|
+
const positionalsByPath = new Map();
|
|
30
|
+
const walk = (s) => {
|
|
31
|
+
subcommandsByPath.set(s.path, s.subcommands);
|
|
32
|
+
optionsByPath.set(s.path, s.options);
|
|
33
|
+
positionalsByPath.set(s.path, s.positionals);
|
|
34
|
+
for (const sub of s.subcommands)
|
|
35
|
+
walk(sub);
|
|
36
|
+
};
|
|
37
|
+
walk(root);
|
|
38
|
+
return { subcommandsByPath, optionsByPath, positionalsByPath };
|
|
39
|
+
}
|
|
40
|
+
function optionAt(tables, path, flag) {
|
|
41
|
+
const opts = tables.optionsByPath.get(path) ?? [];
|
|
42
|
+
return opts.find((o) => o.long === flag || o.short === flag);
|
|
43
|
+
}
|
|
44
|
+
function isKnownSubcommand(tables, path, name) {
|
|
45
|
+
return (tables.subcommandsByPath.get(path) ?? []).some((c) => c.name === name);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Walk the words before the cursor to determine the current command path,
|
|
49
|
+
* skipping options and the values they consume (mirrors the static driver).
|
|
50
|
+
* Returns the resolved command path ("" = root).
|
|
51
|
+
*/
|
|
52
|
+
function resolvePath(tables, words, cword) {
|
|
53
|
+
let path = "";
|
|
54
|
+
let i = 1; // words[0] is the bin name
|
|
55
|
+
while (i < cword) {
|
|
56
|
+
const w = words[i] ?? "";
|
|
57
|
+
if (w.startsWith("-")) {
|
|
58
|
+
const opt = optionAt(tables, path, w);
|
|
59
|
+
if (opt?.takesValue)
|
|
60
|
+
i += 1; // skip the option's value
|
|
61
|
+
}
|
|
62
|
+
else if (isKnownSubcommand(tables, path, w)) {
|
|
63
|
+
path = path ? `${path} ${w}` : w;
|
|
64
|
+
}
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
return path;
|
|
68
|
+
}
|
|
69
|
+
/** Count positional args consumed at the resolved path (non-option, non-subcommand words). */
|
|
70
|
+
function positionalIndex(tables, words, cword) {
|
|
71
|
+
let seen = "";
|
|
72
|
+
let after = 0;
|
|
73
|
+
let j = 1;
|
|
74
|
+
while (j < cword) {
|
|
75
|
+
const w = words[j] ?? "";
|
|
76
|
+
if (w.startsWith("-")) {
|
|
77
|
+
const opt = optionAt(tables, seen, w);
|
|
78
|
+
if (opt?.takesValue)
|
|
79
|
+
j += 1;
|
|
80
|
+
}
|
|
81
|
+
else if (isKnownSubcommand(tables, seen, w)) {
|
|
82
|
+
seen = seen ? `${seen} ${w}` : w;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
after += 1;
|
|
86
|
+
}
|
|
87
|
+
j += 1;
|
|
88
|
+
}
|
|
89
|
+
return after;
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the value source (enum / dynamic-provider / file) for a value slot. */
|
|
92
|
+
async function valueCandidates(source, partial, runProvider) {
|
|
93
|
+
if (source.dynamicProvider) {
|
|
94
|
+
try {
|
|
95
|
+
const values = await runProvider(source.dynamicProvider, partial);
|
|
96
|
+
return { candidates: values.map((value) => ({ value })), directive: Directive.Default };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { candidates: [], directive: Directive.File };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (source.valueChoices && source.valueChoices.length > 0) {
|
|
103
|
+
return {
|
|
104
|
+
candidates: source.valueChoices.map((value) => ({ value })),
|
|
105
|
+
directive: Directive.Default,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// A value is expected but we have nothing to suggest → let the shell try files.
|
|
109
|
+
return { candidates: [], directive: Directive.File };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compute completion candidates for `words` with the cursor at `cword`.
|
|
113
|
+
* `words[0]` is the bin name; `words[cword]` is the (possibly empty) token
|
|
114
|
+
* being completed. The shell shim does the final prefix-filtering against that
|
|
115
|
+
* token, so this returns the full candidate set for the slot.
|
|
116
|
+
*/
|
|
117
|
+
export async function resolveCompletions(program, words, cword, lookup, runProvider) {
|
|
118
|
+
const tables = buildTables(walkProgram(program, lookup));
|
|
119
|
+
const path = resolvePath(tables, words, cword);
|
|
120
|
+
const cur = words[cword] ?? "";
|
|
121
|
+
const prev = cword > 0 ? (words[cword - 1] ?? "") : "";
|
|
122
|
+
// Case 1: previous word is an option that takes a value → complete the value.
|
|
123
|
+
if (prev.startsWith("-")) {
|
|
124
|
+
const opt = optionAt(tables, path, prev);
|
|
125
|
+
if (opt?.takesValue)
|
|
126
|
+
return valueCandidates(opt, cur, runProvider);
|
|
127
|
+
}
|
|
128
|
+
// Case 2: completing an option name (cur starts with "-").
|
|
129
|
+
if (cur.startsWith("-")) {
|
|
130
|
+
const opts = tables.optionsByPath.get(path) ?? [];
|
|
131
|
+
const candidates = [];
|
|
132
|
+
for (const o of opts) {
|
|
133
|
+
if (o.long)
|
|
134
|
+
candidates.push({ value: o.long, description: o.description });
|
|
135
|
+
if (o.short)
|
|
136
|
+
candidates.push({ value: o.short, description: o.description });
|
|
137
|
+
}
|
|
138
|
+
return { candidates, directive: Directive.Default };
|
|
139
|
+
}
|
|
140
|
+
// Case 3: subcommands available at this path → suggest them.
|
|
141
|
+
const subs = tables.subcommandsByPath.get(path) ?? [];
|
|
142
|
+
if (subs.length > 0) {
|
|
143
|
+
return {
|
|
144
|
+
candidates: subs.map((c) => ({ value: c.name, description: c.description })),
|
|
145
|
+
directive: Directive.Default,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// Case 4: positional value slot.
|
|
149
|
+
const positionals = tables.positionalsByPath.get(path) ?? [];
|
|
150
|
+
const idx = positionalIndex(tables, words, cword);
|
|
151
|
+
const pos = positionals[idx] ??
|
|
152
|
+
(positionals[positionals.length - 1]?.variadic
|
|
153
|
+
? positionals[positionals.length - 1]
|
|
154
|
+
: undefined);
|
|
155
|
+
if (pos)
|
|
156
|
+
return valueCandidates(pos, cur, runProvider);
|
|
157
|
+
// Nothing structured to suggest → file completion.
|
|
158
|
+
return { candidates: [], directive: Directive.File };
|
|
159
|
+
}
|
|
160
|
+
/** Sentinel prefix for the trailing directive line in the wire protocol. */
|
|
161
|
+
export const DIRECTIVE_PREFIX = "\x1f:";
|
|
162
|
+
/**
|
|
163
|
+
* Serialize a result for the shell shim: one `value\tdescription` line per
|
|
164
|
+
* candidate, then a final `\x1f:<directive>` line. The \x1f (unit separator)
|
|
165
|
+
* prefix makes the directive line unambiguous against real candidate values.
|
|
166
|
+
*/
|
|
167
|
+
export function encodeResult(result) {
|
|
168
|
+
const lines = result.candidates.map((c) => c.description ? `${c.value}\t${c.description}` : c.value);
|
|
169
|
+
lines.push(`${DIRECTIVE_PREFIX}${result.directive}`);
|
|
170
|
+
return `${lines.join("\n")}\n`;
|
|
171
|
+
}
|
|
@@ -10,4 +10,12 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { CommandSpec } from "./walk.js";
|
|
12
12
|
export declare function generateZsh(root: CommandSpec, binName: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* DYNAMIC zsh completion: a thin, tree-independent shim that calls
|
|
15
|
+
* `<bin> __complete-line` on every <Tab> (see bash.ts generateBashDynamic for
|
|
16
|
+
* the rationale). Candidate lines are `value\tdescription`; the shim converts
|
|
17
|
+
* the tab to `:` so `_describe` renders descriptions. The trailing `\x1f:<n>`
|
|
18
|
+
* line carries the directive (bit 0 = file fallback → `_files`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateZshDynamic(binName: string): string;
|
|
13
21
|
//# sourceMappingURL=zsh.d.ts.map
|