zinkee 0.1.44 → 0.1.45
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/README.md +4 -0
- package/dist/chunk-MHEVRMDX.js +1230 -0
- package/dist/chunk-MHEVRMDX.js.map +1 -0
- package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
- package/dist/examples-E6FFEVBE.js.map +1 -0
- package/dist/index.js +1145 -8456
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/.github/workflows/npm-publish.yml +0 -77
- package/.github/workflows/pr-checks.yml +0 -36
- package/AGENTS.md +0 -110
- package/docs/cli-contract.md +0 -41
- package/docs/npm-release.md +0 -64
- package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
- package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
- package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
- package/src/api/automations.ts +0 -404
- package/src/api/comments.ts +0 -51
- package/src/api/displays.ts +0 -337
- package/src/api/document-templates.ts +0 -110
- package/src/api/files.ts +0 -50
- package/src/api/formulas.test.ts +0 -65
- package/src/api/formulas.ts +0 -67
- package/src/api/logs.ts +0 -34
- package/src/api/navigation.ts +0 -70
- package/src/api/records.ts +0 -184
- package/src/api/schemas.ts +0 -148
- package/src/api/teamspace.ts +0 -110
- package/src/cli-examples.ts +0 -130
- package/src/cli-runner.ts +0 -95
- package/src/client.test.ts +0 -189
- package/src/client.ts +0 -269
- package/src/command-registry.ts +0 -882
- package/src/commands/automations.test.ts +0 -1030
- package/src/commands/automations.ts +0 -2102
- package/src/commands/comments.test.ts +0 -214
- package/src/commands/comments.ts +0 -303
- package/src/commands/config.test.ts +0 -81
- package/src/commands/config.ts +0 -150
- package/src/commands/displays.test.ts +0 -1105
- package/src/commands/displays.ts +0 -1442
- package/src/commands/document-templates.test.ts +0 -569
- package/src/commands/document-templates.ts +0 -563
- package/src/commands/files.test.ts +0 -284
- package/src/commands/files.ts +0 -280
- package/src/commands/formulas.test.ts +0 -194
- package/src/commands/formulas.ts +0 -243
- package/src/commands/logs.test.ts +0 -123
- package/src/commands/logs.ts +0 -159
- package/src/commands/navigation.test.ts +0 -211
- package/src/commands/navigation.ts +0 -348
- package/src/commands/profiles.test.ts +0 -191
- package/src/commands/profiles.ts +0 -303
- package/src/commands/records.test.ts +0 -860
- package/src/commands/records.ts +0 -883
- package/src/commands/schemas.test.ts +0 -1252
- package/src/commands/schemas.ts +0 -890
- package/src/commands/teamspace.test.ts +0 -229
- package/src/commands/teamspace.ts +0 -546
- package/src/completion/engine.test.ts +0 -138
- package/src/completion/engine.ts +0 -168
- package/src/completion/install.test.ts +0 -179
- package/src/completion/install.ts +0 -260
- package/src/completion/runtime.ts +0 -150
- package/src/completion/scripts.ts +0 -91
- package/src/config.test.ts +0 -362
- package/src/config.ts +0 -294
- package/src/index.test.ts +0 -217
- package/src/index.ts +0 -8
- package/src/parsers/expressions.test.ts +0 -95
- package/src/parsers/expressions.ts +0 -128
- package/src/parsers/kv.test.ts +0 -35
- package/src/parsers/kv.ts +0 -49
- package/src/parsers/selectors.test.ts +0 -23
- package/src/parsers/selectors.ts +0 -29
- package/src/program.ts +0 -64
- package/src/runtime-context.ts +0 -103
- package/src/types.ts +0 -76
- package/src/utils/argv-rewrite.test.ts +0 -149
- package/src/utils/argv-rewrite.ts +0 -269
- package/src/utils/errors.test.ts +0 -85
- package/src/utils/errors.ts +0 -191
- package/src/utils/examples.test.ts +0 -374
- package/src/utils/output.test.ts +0 -65
- package/src/utils/output.ts +0 -154
- package/src/utils/schema-fields.ts +0 -1016
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -13
package/src/completion/engine.ts
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,179 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { homedir, platform } from "node:os";
|
|
4
|
-
import { basename, dirname, join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { renderScript, type SupportedShell } from "./scripts.js";
|
|
7
|
-
|
|
8
|
-
export const SUPPORTED_SHELLS: readonly SupportedShell[] = [
|
|
9
|
-
"bash",
|
|
10
|
-
"zsh",
|
|
11
|
-
"fish",
|
|
12
|
-
"powershell",
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const BLOCK_BEGIN = "# >>> zinkee completion >>>";
|
|
16
|
-
const BLOCK_END = "# <<< zinkee completion <<<";
|
|
17
|
-
|
|
18
|
-
export interface InstallEnvironment {
|
|
19
|
-
home: string;
|
|
20
|
-
platform: NodeJS.Platform;
|
|
21
|
-
shellEnv: string | undefined;
|
|
22
|
-
resolvePowerShellProfile?: () => string | undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface InstallTarget {
|
|
26
|
-
shell: SupportedShell;
|
|
27
|
-
scriptPath: string;
|
|
28
|
-
rcPath?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface InstallResult {
|
|
32
|
-
shell: SupportedShell;
|
|
33
|
-
scriptPath: string;
|
|
34
|
-
rcPath?: string;
|
|
35
|
-
rcUpdated: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class CompletionShellError extends Error {
|
|
39
|
-
override name = "CompletionShellError";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function defaultPowerShellProfile(env: InstallEnvironment): string | undefined {
|
|
43
|
-
for (const exe of ["pwsh", "powershell.exe"]) {
|
|
44
|
-
try {
|
|
45
|
-
const out = execFileSync(exe, ["-NoProfile", "-Command", "$PROFILE"], {
|
|
46
|
-
encoding: "utf8",
|
|
47
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
48
|
-
});
|
|
49
|
-
const path = out.trim();
|
|
50
|
-
if (path) {
|
|
51
|
-
return path;
|
|
52
|
-
}
|
|
53
|
-
} catch {
|
|
54
|
-
// try the next executable
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (env.platform === "win32") {
|
|
59
|
-
return join(env.home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
|
|
60
|
-
}
|
|
61
|
-
return join(env.home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function detectShell(env: InstallEnvironment): SupportedShell {
|
|
65
|
-
const fromEnv = env.shellEnv;
|
|
66
|
-
if (fromEnv) {
|
|
67
|
-
const name = basename(fromEnv).toLowerCase().replace(/\.exe$/, "");
|
|
68
|
-
if (name === "bash") {
|
|
69
|
-
return "bash";
|
|
70
|
-
}
|
|
71
|
-
if (name === "zsh") {
|
|
72
|
-
return "zsh";
|
|
73
|
-
}
|
|
74
|
-
if (name === "fish") {
|
|
75
|
-
return "fish";
|
|
76
|
-
}
|
|
77
|
-
if (name === "pwsh" || name === "powershell") {
|
|
78
|
-
return "powershell";
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (env.platform === "win32") {
|
|
82
|
-
return "powershell";
|
|
83
|
-
}
|
|
84
|
-
throw new CompletionShellError(
|
|
85
|
-
`Could not detect a supported shell from $SHELL=${fromEnv ?? ""}. ` +
|
|
86
|
-
`Pass one of: ${SUPPORTED_SHELLS.join(", ")}.`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function parseShell(value: string): SupportedShell {
|
|
91
|
-
const normalized = value.toLowerCase();
|
|
92
|
-
if (
|
|
93
|
-
normalized === "bash" ||
|
|
94
|
-
normalized === "zsh" ||
|
|
95
|
-
normalized === "fish" ||
|
|
96
|
-
normalized === "powershell"
|
|
97
|
-
) {
|
|
98
|
-
return normalized;
|
|
99
|
-
}
|
|
100
|
-
if (normalized === "pwsh") {
|
|
101
|
-
return "powershell";
|
|
102
|
-
}
|
|
103
|
-
throw new CompletionShellError(
|
|
104
|
-
`Unsupported shell "${value}". Supported: ${SUPPORTED_SHELLS.join(", ")}.`,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function resolveTarget(
|
|
109
|
-
shell: SupportedShell,
|
|
110
|
-
env: InstallEnvironment,
|
|
111
|
-
): InstallTarget {
|
|
112
|
-
switch (shell) {
|
|
113
|
-
case "bash":
|
|
114
|
-
return {
|
|
115
|
-
shell,
|
|
116
|
-
scriptPath: join(env.home, ".bash_completions", "zinkee.sh"),
|
|
117
|
-
rcPath: join(env.home, ".bashrc"),
|
|
118
|
-
};
|
|
119
|
-
case "zsh":
|
|
120
|
-
return {
|
|
121
|
-
shell,
|
|
122
|
-
scriptPath: join(env.home, ".zfunc", "_zinkee"),
|
|
123
|
-
rcPath: join(env.home, ".zshrc"),
|
|
124
|
-
};
|
|
125
|
-
case "fish":
|
|
126
|
-
return {
|
|
127
|
-
shell,
|
|
128
|
-
scriptPath: join(env.home, ".config", "fish", "completions", "zinkee.fish"),
|
|
129
|
-
};
|
|
130
|
-
case "powershell": {
|
|
131
|
-
const resolver = env.resolvePowerShellProfile ?? (() => defaultPowerShellProfile(env));
|
|
132
|
-
const profilePath = resolver();
|
|
133
|
-
if (!profilePath) {
|
|
134
|
-
throw new CompletionShellError(
|
|
135
|
-
"Could not resolve PowerShell $PROFILE path. Install PowerShell or use --show-completion.",
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
return {
|
|
139
|
-
shell,
|
|
140
|
-
scriptPath: profilePath,
|
|
141
|
-
rcPath: profilePath,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function buildRcSnippet(shell: SupportedShell, scriptPath: string): string {
|
|
148
|
-
switch (shell) {
|
|
149
|
-
case "bash":
|
|
150
|
-
return `source "${scriptPath}"`;
|
|
151
|
-
case "zsh":
|
|
152
|
-
return [
|
|
153
|
-
`fpath=("${dirname(scriptPath)}" $fpath)`,
|
|
154
|
-
"autoload -Uz compinit && compinit",
|
|
155
|
-
].join("\n");
|
|
156
|
-
default:
|
|
157
|
-
return "";
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function applyBlock(existing: string, block: string): string {
|
|
162
|
-
const wrapped = `${BLOCK_BEGIN}\n${block.trimEnd()}\n${BLOCK_END}\n`;
|
|
163
|
-
const blockPattern = new RegExp(
|
|
164
|
-
`${BLOCK_BEGIN.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`,
|
|
165
|
-
"m",
|
|
166
|
-
);
|
|
167
|
-
if (blockPattern.test(existing)) {
|
|
168
|
-
return existing.replace(blockPattern, wrapped);
|
|
169
|
-
}
|
|
170
|
-
const separator = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
171
|
-
return `${existing}${separator}${wrapped}`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function readFileOrEmpty(path: string): string {
|
|
175
|
-
try {
|
|
176
|
-
return readFileSync(path, "utf8");
|
|
177
|
-
} catch (error) {
|
|
178
|
-
if (
|
|
179
|
-
typeof error === "object" &&
|
|
180
|
-
error !== null &&
|
|
181
|
-
"code" in error &&
|
|
182
|
-
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
183
|
-
) {
|
|
184
|
-
return "";
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function writeFileEnsuringDir(path: string, content: string): void {
|
|
191
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
192
|
-
writeFileSync(path, content, "utf8");
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export function installCompletion(
|
|
196
|
-
shell: SupportedShell,
|
|
197
|
-
env: InstallEnvironment,
|
|
198
|
-
): InstallResult {
|
|
199
|
-
const target = resolveTarget(shell, env);
|
|
200
|
-
const script = renderScript(shell);
|
|
201
|
-
|
|
202
|
-
if (shell === "powershell") {
|
|
203
|
-
const existing = readFileOrEmpty(target.scriptPath);
|
|
204
|
-
const updated = applyBlock(existing, script);
|
|
205
|
-
writeFileEnsuringDir(target.scriptPath, updated);
|
|
206
|
-
return {
|
|
207
|
-
shell,
|
|
208
|
-
scriptPath: target.scriptPath,
|
|
209
|
-
rcPath: target.scriptPath,
|
|
210
|
-
rcUpdated: true,
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
writeFileEnsuringDir(target.scriptPath, script);
|
|
215
|
-
|
|
216
|
-
if (!target.rcPath) {
|
|
217
|
-
return {
|
|
218
|
-
shell,
|
|
219
|
-
scriptPath: target.scriptPath,
|
|
220
|
-
rcUpdated: false,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const snippet = buildRcSnippet(shell, target.scriptPath);
|
|
225
|
-
if (!snippet) {
|
|
226
|
-
return {
|
|
227
|
-
shell,
|
|
228
|
-
scriptPath: target.scriptPath,
|
|
229
|
-
rcPath: target.rcPath,
|
|
230
|
-
rcUpdated: false,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const existingRc = readFileOrEmpty(target.rcPath);
|
|
235
|
-
const updatedRc = applyBlock(existingRc, snippet);
|
|
236
|
-
if (updatedRc !== existingRc) {
|
|
237
|
-
writeFileEnsuringDir(target.rcPath, updatedRc);
|
|
238
|
-
return {
|
|
239
|
-
shell,
|
|
240
|
-
scriptPath: target.scriptPath,
|
|
241
|
-
rcPath: target.rcPath,
|
|
242
|
-
rcUpdated: true,
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return {
|
|
247
|
-
shell,
|
|
248
|
-
scriptPath: target.scriptPath,
|
|
249
|
-
rcPath: target.rcPath,
|
|
250
|
-
rcUpdated: false,
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function currentEnvironment(): InstallEnvironment {
|
|
255
|
-
return {
|
|
256
|
-
home: homedir(),
|
|
257
|
-
platform: platform(),
|
|
258
|
-
shellEnv: process.env.SHELL,
|
|
259
|
-
};
|
|
260
|
-
}
|