zinkee 0.1.10 → 0.1.11
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/.claude/settings.local.json +29 -1
- package/AGENTS.md +1 -0
- package/README.md +12 -0
- package/dist/index.js +544 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli-runner.ts +18 -0
- package/src/completion/engine.test.ts +138 -0
- package/src/completion/engine.ts +168 -0
- package/src/completion/install.test.ts +179 -0
- package/src/completion/install.ts +260 -0
- package/src/completion/runtime.ts +150 -0
- package/src/completion/scripts.ts +91 -0
- package/src/program.ts +3 -1
package/package.json
CHANGED
package/src/cli-runner.ts
CHANGED
|
@@ -4,6 +4,12 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
|
|
6
6
|
import { extractExampleRequest, showExamples } from "./cli-examples.js";
|
|
7
|
+
import {
|
|
8
|
+
extractCompletionFlag,
|
|
9
|
+
handleCompletionFlag,
|
|
10
|
+
isCompletionRequest,
|
|
11
|
+
runCompletionRequest,
|
|
12
|
+
} from "./completion/runtime.js";
|
|
7
13
|
import { buildProgram } from "./program.js";
|
|
8
14
|
import {
|
|
9
15
|
formatCliErrorJson,
|
|
@@ -37,6 +43,18 @@ export function isExecutedAsEntry(argvEntry: string | undefined, moduleUrl: stri
|
|
|
37
43
|
|
|
38
44
|
export async function runCli(argv: string[]): Promise<void> {
|
|
39
45
|
try {
|
|
46
|
+
if (isCompletionRequest()) {
|
|
47
|
+
runCompletionRequest();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const completionFlag = extractCompletionFlag(argv);
|
|
52
|
+
if (completionFlag) {
|
|
53
|
+
const exitCode = handleCompletionFlag(completionFlag);
|
|
54
|
+
process.exitCode = exitCode;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
40
58
|
const exampleRequest = extractExampleRequest(argv);
|
|
41
59
|
|
|
42
60
|
if (exampleRequest) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildProgram } from "../program.js";
|
|
4
|
+
import { getCompletions } from "./engine.js";
|
|
5
|
+
|
|
6
|
+
const NEVER_READ_PROFILES = (): readonly string[] => [];
|
|
7
|
+
|
|
8
|
+
describe("completion engine", () => {
|
|
9
|
+
it("suggests top-level subcommands and inherited globals", () => {
|
|
10
|
+
const program = buildProgram();
|
|
11
|
+
const result = getCompletions(
|
|
12
|
+
program,
|
|
13
|
+
{ words: ["zinkee", ""], cwordIndex: 1 },
|
|
14
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
expect(result).toContain("schemas");
|
|
18
|
+
expect(result).toContain("automations");
|
|
19
|
+
expect(result).toContain("--json");
|
|
20
|
+
expect(result).toContain("--profile");
|
|
21
|
+
expect(result).toContain("--help");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("filters subcommands by prefix", () => {
|
|
25
|
+
const program = buildProgram();
|
|
26
|
+
const result = getCompletions(
|
|
27
|
+
program,
|
|
28
|
+
{ words: ["zinkee", "sch"], cwordIndex: 1 },
|
|
29
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual(["schemas"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("descends into subcommands", () => {
|
|
36
|
+
const program = buildProgram();
|
|
37
|
+
const result = getCompletions(
|
|
38
|
+
program,
|
|
39
|
+
{ words: ["zinkee", "schemas", ""], cwordIndex: 2 },
|
|
40
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(result).toContain("list");
|
|
44
|
+
expect(result).toContain("get");
|
|
45
|
+
expect(result).toContain("create");
|
|
46
|
+
expect(result).toContain("fields");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("descends into nested subcommands", () => {
|
|
50
|
+
const program = buildProgram();
|
|
51
|
+
const result = getCompletions(
|
|
52
|
+
program,
|
|
53
|
+
{ words: ["zinkee", "schemas", "fields", ""], cwordIndex: 3 },
|
|
54
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(result).toContain("list");
|
|
58
|
+
expect(result).toContain("create");
|
|
59
|
+
expect(result).toContain("update");
|
|
60
|
+
expect(result).toContain("delete");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("only suggests long flags when the current token starts with --", () => {
|
|
64
|
+
const program = buildProgram();
|
|
65
|
+
const result = getCompletions(
|
|
66
|
+
program,
|
|
67
|
+
{ words: ["zinkee", "schemas", "list", "--"], cwordIndex: 3 },
|
|
68
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(result.every((candidate) => candidate.startsWith("--"))).toBe(true);
|
|
72
|
+
expect(result).toContain("--json");
|
|
73
|
+
expect(result).toContain("--profile");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips flag tokens when descending into the command tree", () => {
|
|
77
|
+
const program = buildProgram();
|
|
78
|
+
const result = getCompletions(
|
|
79
|
+
program,
|
|
80
|
+
{
|
|
81
|
+
words: ["zinkee", "--profile", "prod", "schemas", ""],
|
|
82
|
+
cwordIndex: 4,
|
|
83
|
+
},
|
|
84
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(result).toContain("list");
|
|
88
|
+
expect(result).toContain("fields");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("returns profile names when the previous token is --profile", () => {
|
|
92
|
+
const program = buildProgram();
|
|
93
|
+
const result = getCompletions(
|
|
94
|
+
program,
|
|
95
|
+
{ words: ["zinkee", "--profile", ""], cwordIndex: 2 },
|
|
96
|
+
{ readProfileNames: () => ["alpha", "beta"] },
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual(["alpha", "beta"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("filters profile names by prefix", () => {
|
|
103
|
+
const program = buildProgram();
|
|
104
|
+
const result = getCompletions(
|
|
105
|
+
program,
|
|
106
|
+
{ words: ["zinkee", "--profile", "be"], cwordIndex: 2 },
|
|
107
|
+
{ readProfileNames: () => ["alpha", "beta"] },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
expect(result).toEqual(["beta"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not crash when profile reader throws", () => {
|
|
114
|
+
const program = buildProgram();
|
|
115
|
+
const result = getCompletions(
|
|
116
|
+
program,
|
|
117
|
+
{ words: ["zinkee", "--profile", ""], cwordIndex: 2 },
|
|
118
|
+
{
|
|
119
|
+
readProfileNames: () => {
|
|
120
|
+
throw new Error("config unreadable");
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(result).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("ignores hidden and unknown subcommands", () => {
|
|
129
|
+
const program = buildProgram();
|
|
130
|
+
const result = getCompletions(
|
|
131
|
+
program,
|
|
132
|
+
{ words: ["zinkee", "no-such-thing", ""], cwordIndex: 2 },
|
|
133
|
+
{ readProfileNames: NEVER_READ_PROFILES },
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(result).toContain("schemas");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
|
|
3
|
+
import { readConfigIfExists } from "../config.js";
|
|
4
|
+
|
|
5
|
+
export interface CompletionRequest {
|
|
6
|
+
words: readonly string[];
|
|
7
|
+
cwordIndex: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CompletionDeps {
|
|
11
|
+
configPath?: string;
|
|
12
|
+
readProfileNames?: (configPath: string | undefined) => readonly string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readProfileNamesFromConfig(configPath: string | undefined): readonly string[] {
|
|
16
|
+
try {
|
|
17
|
+
const config = readConfigIfExists(configPath);
|
|
18
|
+
if (!config) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(config.profiles).sort();
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findSubcommand(command: Command, name: string): Command | undefined {
|
|
28
|
+
return command.commands.find((sub) => {
|
|
29
|
+
if (sub.name() === name) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
const aliases = sub.aliases();
|
|
33
|
+
return aliases.includes(name);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectFlagsThatTakeValues(root: Command): Set<string> {
|
|
38
|
+
const flags = new Set<string>();
|
|
39
|
+
const walk = (command: Command): void => {
|
|
40
|
+
for (const option of command.options) {
|
|
41
|
+
if (!option.required && !option.optional) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (option.long) {
|
|
45
|
+
flags.add(option.long);
|
|
46
|
+
}
|
|
47
|
+
if (option.short) {
|
|
48
|
+
flags.add(option.short);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const sub of command.commands) {
|
|
52
|
+
walk(sub);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
walk(root);
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function descend(root: Command, words: readonly string[]): Command {
|
|
60
|
+
const flagsWithValues = collectFlagsThatTakeValues(root);
|
|
61
|
+
let current = root;
|
|
62
|
+
let index = 0;
|
|
63
|
+
|
|
64
|
+
while (index < words.length) {
|
|
65
|
+
const word = words[index]!;
|
|
66
|
+
if (word.startsWith("-")) {
|
|
67
|
+
const [flagPart] = word.split("=", 1);
|
|
68
|
+
if (flagPart && flagsWithValues.has(flagPart) && !word.includes("=")) {
|
|
69
|
+
index += 2;
|
|
70
|
+
} else {
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const next = findSubcommand(current, word);
|
|
76
|
+
if (!next) {
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
current = next;
|
|
80
|
+
index += 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return current;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collectVisibleSubcommands(command: Command): readonly string[] {
|
|
87
|
+
const names: string[] = [];
|
|
88
|
+
for (const sub of command.commands) {
|
|
89
|
+
if (sub.name().startsWith("__")) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
names.push(sub.name());
|
|
93
|
+
}
|
|
94
|
+
return names;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function collectVisibleLongFlags(command: Command): readonly string[] {
|
|
98
|
+
const flags: string[] = [];
|
|
99
|
+
let current: Command | null = command;
|
|
100
|
+
|
|
101
|
+
while (current) {
|
|
102
|
+
for (const option of current.options) {
|
|
103
|
+
if (option.hidden || !option.long) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!flags.includes(option.long)) {
|
|
107
|
+
flags.push(option.long);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
current = current.parent;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!flags.includes("--help")) {
|
|
114
|
+
flags.push("--help");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return flags;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function getCompletions(
|
|
121
|
+
program: Command,
|
|
122
|
+
request: CompletionRequest,
|
|
123
|
+
deps: CompletionDeps = {},
|
|
124
|
+
): string[] {
|
|
125
|
+
const { words, cwordIndex } = request;
|
|
126
|
+
const safeIndex = Math.max(0, Math.min(cwordIndex, words.length));
|
|
127
|
+
const current = words[safeIndex] ?? "";
|
|
128
|
+
const prevToken = safeIndex > 0 ? words[safeIndex - 1] : undefined;
|
|
129
|
+
const readProfiles = deps.readProfileNames ?? readProfileNamesFromConfig;
|
|
130
|
+
|
|
131
|
+
if (prevToken === "--profile" || prevToken === "-p") {
|
|
132
|
+
try {
|
|
133
|
+
return readProfiles(deps.configPath)
|
|
134
|
+
.filter((name) => name.startsWith(current))
|
|
135
|
+
.slice()
|
|
136
|
+
.sort();
|
|
137
|
+
} catch {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const tokensBeforeCurrent = words.slice(1, safeIndex);
|
|
143
|
+
const command = descend(program, tokensBeforeCurrent);
|
|
144
|
+
|
|
145
|
+
const candidates = new Set<string>();
|
|
146
|
+
|
|
147
|
+
if (current.startsWith("-")) {
|
|
148
|
+
for (const flag of collectVisibleLongFlags(command)) {
|
|
149
|
+
if (flag.startsWith(current)) {
|
|
150
|
+
candidates.add(flag);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [...candidates].sort();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const subcommand of collectVisibleSubcommands(command)) {
|
|
157
|
+
if (subcommand.startsWith(current)) {
|
|
158
|
+
candidates.add(subcommand);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const flag of collectVisibleLongFlags(command)) {
|
|
162
|
+
if (flag.startsWith(current)) {
|
|
163
|
+
candidates.add(flag);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return [...candidates].sort();
|
|
168
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
CompletionShellError,
|
|
9
|
+
detectShell,
|
|
10
|
+
installCompletion,
|
|
11
|
+
parseShell,
|
|
12
|
+
resolveTarget,
|
|
13
|
+
type InstallEnvironment,
|
|
14
|
+
} from "./install.js";
|
|
15
|
+
|
|
16
|
+
const tempDirs: string[] = [];
|
|
17
|
+
|
|
18
|
+
function createTempHome(): string {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "zinkee-install-"));
|
|
20
|
+
tempDirs.push(dir);
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function envFor(home: string, overrides: Partial<InstallEnvironment> = {}): InstallEnvironment {
|
|
25
|
+
return {
|
|
26
|
+
home,
|
|
27
|
+
platform: "linux",
|
|
28
|
+
shellEnv: "/bin/zsh",
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
while (tempDirs.length > 0) {
|
|
35
|
+
rmSync(tempDirs.pop()!, { force: true, recursive: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("detectShell", () => {
|
|
40
|
+
it("detects zsh from $SHELL", () => {
|
|
41
|
+
expect(detectShell({ home: "", platform: "linux", shellEnv: "/usr/bin/zsh" })).toBe("zsh");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("detects bash from $SHELL", () => {
|
|
45
|
+
expect(detectShell({ home: "", platform: "linux", shellEnv: "/bin/bash" })).toBe("bash");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("detects fish from $SHELL", () => {
|
|
49
|
+
expect(detectShell({ home: "", platform: "linux", shellEnv: "/usr/local/bin/fish" })).toBe(
|
|
50
|
+
"fish",
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("detects powershell from pwsh", () => {
|
|
55
|
+
expect(detectShell({ home: "", platform: "linux", shellEnv: "/usr/bin/pwsh" })).toBe(
|
|
56
|
+
"powershell",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("falls back to powershell on win32 when $SHELL is unknown", () => {
|
|
61
|
+
expect(detectShell({ home: "", platform: "win32", shellEnv: undefined })).toBe("powershell");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("throws when shell cannot be inferred", () => {
|
|
65
|
+
expect(() =>
|
|
66
|
+
detectShell({ home: "", platform: "linux", shellEnv: "/bin/csh" }),
|
|
67
|
+
).toThrow(CompletionShellError);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("parseShell", () => {
|
|
72
|
+
it("normalizes pwsh to powershell", () => {
|
|
73
|
+
expect(parseShell("pwsh")).toBe("powershell");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rejects unsupported names", () => {
|
|
77
|
+
expect(() => parseShell("nu")).toThrow(CompletionShellError);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("resolveTarget", () => {
|
|
82
|
+
it("uses $PROFILE resolver for powershell", () => {
|
|
83
|
+
const home = createTempHome();
|
|
84
|
+
const target = resolveTarget(
|
|
85
|
+
"powershell",
|
|
86
|
+
envFor(home, {
|
|
87
|
+
platform: "win32",
|
|
88
|
+
resolvePowerShellProfile: () => join(home, "Documents", "PowerShell", "profile.ps1"),
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
expect(target.scriptPath).toBe(join(home, "Documents", "PowerShell", "profile.ps1"));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("installCompletion", () => {
|
|
96
|
+
it("installs zsh completion and updates rc with idempotent block", () => {
|
|
97
|
+
const home = createTempHome();
|
|
98
|
+
writeFileSync(join(home, ".zshrc"), "# user content\n", "utf8");
|
|
99
|
+
|
|
100
|
+
const first = installCompletion("zsh", envFor(home));
|
|
101
|
+
expect(first.rcUpdated).toBe(true);
|
|
102
|
+
const rcAfter = readFileSync(join(home, ".zshrc"), "utf8");
|
|
103
|
+
expect(rcAfter).toContain("# >>> zinkee completion >>>");
|
|
104
|
+
expect(rcAfter).toContain(`fpath=("${join(home, ".zfunc")}" $fpath)`);
|
|
105
|
+
expect(rcAfter).toContain("autoload -Uz compinit && compinit");
|
|
106
|
+
expect(rcAfter).toContain("# user content");
|
|
107
|
+
|
|
108
|
+
const second = installCompletion("zsh", envFor(home));
|
|
109
|
+
expect(second.rcUpdated).toBe(false);
|
|
110
|
+
const rcSecond = readFileSync(join(home, ".zshrc"), "utf8");
|
|
111
|
+
const beginCount = (rcSecond.match(/# >>> zinkee completion >>>/g) ?? []).length;
|
|
112
|
+
expect(beginCount).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("installs bash completion to ~/.bash_completions and sources it from .bashrc", () => {
|
|
116
|
+
const home = createTempHome();
|
|
117
|
+
const result = installCompletion("bash", envFor(home, { shellEnv: "/bin/bash" }));
|
|
118
|
+
expect(result.scriptPath).toBe(join(home, ".bash_completions", "zinkee.sh"));
|
|
119
|
+
expect(result.rcPath).toBe(join(home, ".bashrc"));
|
|
120
|
+
|
|
121
|
+
const rc = readFileSync(join(home, ".bashrc"), "utf8");
|
|
122
|
+
expect(rc).toContain(`source "${join(home, ".bash_completions", "zinkee.sh")}"`);
|
|
123
|
+
|
|
124
|
+
const script = readFileSync(result.scriptPath, "utf8");
|
|
125
|
+
expect(script).toContain("complete -o default -F _zinkee_completion zinkee");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("installs fish completion without touching any rc file", () => {
|
|
129
|
+
const home = createTempHome();
|
|
130
|
+
const result = installCompletion("fish", envFor(home, { shellEnv: "/usr/bin/fish" }));
|
|
131
|
+
expect(result.scriptPath).toBe(
|
|
132
|
+
join(home, ".config", "fish", "completions", "zinkee.fish"),
|
|
133
|
+
);
|
|
134
|
+
expect(result.rcUpdated).toBe(false);
|
|
135
|
+
|
|
136
|
+
const script = readFileSync(result.scriptPath, "utf8");
|
|
137
|
+
expect(script).toContain("complete --no-files --command zinkee");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("inlines powershell completion inside the resolved $PROFILE", () => {
|
|
141
|
+
const home = createTempHome();
|
|
142
|
+
const profilePath = join(home, "Documents", "PowerShell", "profile.ps1");
|
|
143
|
+
const result = installCompletion(
|
|
144
|
+
"powershell",
|
|
145
|
+
envFor(home, {
|
|
146
|
+
platform: "win32",
|
|
147
|
+
shellEnv: undefined,
|
|
148
|
+
resolvePowerShellProfile: () => profilePath,
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result.scriptPath).toBe(profilePath);
|
|
153
|
+
const content = readFileSync(profilePath, "utf8");
|
|
154
|
+
expect(content).toContain("# >>> zinkee completion >>>");
|
|
155
|
+
expect(content).toContain("Register-ArgumentCompleter -Native -CommandName zinkee");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("replaces an existing block instead of appending a new one", () => {
|
|
159
|
+
const home = createTempHome();
|
|
160
|
+
writeFileSync(
|
|
161
|
+
join(home, ".zshrc"),
|
|
162
|
+
[
|
|
163
|
+
"# top",
|
|
164
|
+
"# >>> zinkee completion >>>",
|
|
165
|
+
"stale-content",
|
|
166
|
+
"# <<< zinkee completion <<<",
|
|
167
|
+
"# bottom",
|
|
168
|
+
].join("\n") + "\n",
|
|
169
|
+
"utf8",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
installCompletion("zsh", envFor(home));
|
|
173
|
+
const rc = readFileSync(join(home, ".zshrc"), "utf8");
|
|
174
|
+
expect(rc).not.toContain("stale-content");
|
|
175
|
+
expect(rc).toContain("# top");
|
|
176
|
+
expect(rc).toContain("# bottom");
|
|
177
|
+
expect((rc.match(/# >>> zinkee completion >>>/g) ?? []).length).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
});
|