zinkee 0.1.10 → 0.1.12

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.
@@ -0,0 +1,260 @@
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
+ }
@@ -0,0 +1,150 @@
1
+ import type { Writable } from "node:stream";
2
+
3
+ import { buildProgram } from "../program.js";
4
+ import { getCompletions } from "./engine.js";
5
+ import {
6
+ CompletionShellError,
7
+ currentEnvironment,
8
+ detectShell,
9
+ installCompletion,
10
+ parseShell,
11
+ type InstallEnvironment,
12
+ } from "./install.js";
13
+ import { renderScript, type SupportedShell } from "./scripts.js";
14
+
15
+ export const COMPLETION_ENV_VAR = "_ZINKEE_COMPLETE";
16
+
17
+ interface Stream {
18
+ write(chunk: string): unknown;
19
+ }
20
+
21
+ function getStream(stream: Stream | undefined, fallback: NodeJS.WriteStream): Stream {
22
+ return stream ?? (fallback as unknown as Writable);
23
+ }
24
+
25
+ function parseEnvWords(env: NodeJS.ProcessEnv): string[] {
26
+ const raw = env.COMP_WORDS ?? "";
27
+ if (raw.trim().length === 0) {
28
+ return [];
29
+ }
30
+ return raw.split(/\s+/).filter((token) => token.length > 0);
31
+ }
32
+
33
+ function parseEnvCwordIndex(env: NodeJS.ProcessEnv): number {
34
+ const raw = env.COMP_CWORD ?? "0";
35
+ const parsed = Number.parseInt(raw, 10);
36
+ if (Number.isNaN(parsed) || parsed < 0) {
37
+ return 0;
38
+ }
39
+ return parsed;
40
+ }
41
+
42
+ export function isCompletionRequest(env: NodeJS.ProcessEnv = process.env): boolean {
43
+ return typeof env[COMPLETION_ENV_VAR] === "string" && env[COMPLETION_ENV_VAR]!.length > 0;
44
+ }
45
+
46
+ export function runCompletionRequest(
47
+ env: NodeJS.ProcessEnv = process.env,
48
+ stdout?: Stream,
49
+ ): void {
50
+ const out = getStream(stdout, process.stdout);
51
+ try {
52
+ const program = buildProgram();
53
+ const candidates = getCompletions(program, {
54
+ words: parseEnvWords(env),
55
+ cwordIndex: parseEnvCwordIndex(env),
56
+ });
57
+ if (candidates.length > 0) {
58
+ out.write(`${candidates.join("\n")}\n`);
59
+ }
60
+ } catch {
61
+ // Completion must never crash the user's shell. Swallow.
62
+ }
63
+ }
64
+
65
+ export interface CompletionFlagRequest {
66
+ mode: "install" | "show";
67
+ shellOverride?: string;
68
+ }
69
+
70
+ export function extractCompletionFlag(argv: readonly string[]): CompletionFlagRequest | null {
71
+ for (let index = 0; index < argv.length; index += 1) {
72
+ const token = argv[index];
73
+ if (!token) {
74
+ continue;
75
+ }
76
+ if (token === "--install-completion" || token === "--show-completion") {
77
+ const mode = token === "--install-completion" ? "install" : "show";
78
+ const next = argv[index + 1];
79
+ const shellOverride = next && !next.startsWith("-") ? next : undefined;
80
+ return { mode, shellOverride };
81
+ }
82
+ if (token.startsWith("--install-completion=")) {
83
+ const value = token.slice("--install-completion=".length);
84
+ return { mode: "install", shellOverride: value || undefined };
85
+ }
86
+ if (token.startsWith("--show-completion=")) {
87
+ const value = token.slice("--show-completion=".length);
88
+ return { mode: "show", shellOverride: value || undefined };
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function resolveShell(
95
+ request: CompletionFlagRequest,
96
+ env: InstallEnvironment,
97
+ ): SupportedShell {
98
+ if (request.shellOverride) {
99
+ return parseShell(request.shellOverride);
100
+ }
101
+ return detectShell(env);
102
+ }
103
+
104
+ export interface CompletionFlagDeps {
105
+ environment?: InstallEnvironment;
106
+ stdout?: Stream;
107
+ stderr?: Stream;
108
+ }
109
+
110
+ export function handleCompletionFlag(
111
+ request: CompletionFlagRequest,
112
+ deps: CompletionFlagDeps = {},
113
+ ): number {
114
+ const env = deps.environment ?? currentEnvironment();
115
+ const out = getStream(deps.stdout, process.stdout);
116
+ const err = getStream(deps.stderr, process.stderr);
117
+
118
+ let shell: SupportedShell;
119
+ try {
120
+ shell = resolveShell(request, env);
121
+ } catch (error) {
122
+ const message = error instanceof CompletionShellError ? error.message : String(error);
123
+ err.write(`${message}\n`);
124
+ return 1;
125
+ }
126
+
127
+ if (request.mode === "show") {
128
+ out.write(renderScript(shell));
129
+ return 0;
130
+ }
131
+
132
+ try {
133
+ const result = installCompletion(shell, env);
134
+ err.write(`zinkee completion installed for ${shell}.\n`);
135
+ err.write(` script: ${result.scriptPath}\n`);
136
+ if (result.rcPath && result.rcPath !== result.scriptPath) {
137
+ err.write(
138
+ result.rcUpdated
139
+ ? ` updated: ${result.rcPath}\n`
140
+ : ` unchanged: ${result.rcPath}\n`,
141
+ );
142
+ }
143
+ err.write(`Restart your shell or open a new session to enable completion.\n`);
144
+ return 0;
145
+ } catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ err.write(`Failed to install zinkee completion: ${message}\n`);
148
+ return 1;
149
+ }
150
+ }
@@ -0,0 +1,91 @@
1
+ export type SupportedShell = "bash" | "zsh" | "fish" | "powershell";
2
+
3
+ const PROGRAM = "zinkee";
4
+ const ENV_VAR = "_ZINKEE_COMPLETE";
5
+
6
+ export function renderBashScript(): string {
7
+ return `_zinkee_completion() {
8
+ local IFS=$'\\n'
9
+ local response
10
+
11
+ response=$(env COMP_WORDS="\${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD ${ENV_VAR}=bash_complete ${PROGRAM} 2>/dev/null)
12
+
13
+ COMPREPLY=( \${response} )
14
+ return 0
15
+ }
16
+
17
+ complete -o default -F _zinkee_completion ${PROGRAM}
18
+ `;
19
+ }
20
+
21
+ export function renderZshScript(): string {
22
+ return `#compdef ${PROGRAM}
23
+
24
+ _zinkee_completion() {
25
+ local -a response
26
+ (( ! $+commands[${PROGRAM}] )) && return 1
27
+
28
+ response=("\${(@f)$(env COMP_WORDS="\${words[*]}" COMP_CWORD=$((CURRENT - 1)) ${ENV_VAR}=zsh_complete ${PROGRAM} 2>/dev/null)}")
29
+
30
+ if [ -n "$response" ]; then
31
+ compadd -U -V unsorted -a response
32
+ fi
33
+ }
34
+
35
+ if [[ $zsh_eval_context[-1] == loadautofunc ]]; then
36
+ _zinkee_completion "$@"
37
+ else
38
+ compdef _zinkee_completion ${PROGRAM}
39
+ fi
40
+ `;
41
+ }
42
+
43
+ export function renderFishScript(): string {
44
+ return `function __zinkee_completion
45
+ set -l tokens (commandline -opc) (commandline -ct)
46
+ set -l cword (math (count $tokens) - 1)
47
+ set -l joined (string join " " -- $tokens)
48
+
49
+ env COMP_WORDS="$joined" COMP_CWORD=$cword ${ENV_VAR}=fish_complete ${PROGRAM} 2>/dev/null
50
+ end
51
+
52
+ complete --no-files --command ${PROGRAM} --arguments '(__zinkee_completion)'
53
+ `;
54
+ }
55
+
56
+ export function renderPowerShellScript(): string {
57
+ return `Register-ArgumentCompleter -Native -CommandName ${PROGRAM} -ScriptBlock {
58
+ param($wordToComplete, $commandAst, $cursorPosition)
59
+
60
+ $tokens = @($commandAst.CommandElements | ForEach-Object { $_.ToString() })
61
+ $cword = [Math]::Max(0, $tokens.Count - 1)
62
+
63
+ $env:COMP_WORDS = ($tokens -join ' ')
64
+ $env:COMP_CWORD = $cword
65
+ $env:${ENV_VAR} = 'powershell_complete'
66
+
67
+ try {
68
+ & ${PROGRAM} 2>$null | ForEach-Object {
69
+ [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
70
+ }
71
+ } finally {
72
+ Remove-Item Env:COMP_WORDS -ErrorAction SilentlyContinue
73
+ Remove-Item Env:COMP_CWORD -ErrorAction SilentlyContinue
74
+ Remove-Item Env:${ENV_VAR} -ErrorAction SilentlyContinue
75
+ }
76
+ }
77
+ `;
78
+ }
79
+
80
+ export function renderScript(shell: SupportedShell): string {
81
+ switch (shell) {
82
+ case "bash":
83
+ return renderBashScript();
84
+ case "zsh":
85
+ return renderZshScript();
86
+ case "fish":
87
+ return renderFishScript();
88
+ case "powershell":
89
+ return renderPowerShellScript();
90
+ }
91
+ }
package/src/index.test.ts CHANGED
@@ -69,6 +69,58 @@ describe("project bootstrap", () => {
69
69
  });
70
70
  });
71
71
 
72
+ it("extracts example requests when --example appears at the end of argv", () => {
73
+ expect(
74
+ extractExampleRequest(["node", "zinkee", "schemas", "list", "--example"]),
75
+ ).toEqual({
76
+ commandName: "schemas list",
77
+ filters: {},
78
+ overrides: {},
79
+ json: false,
80
+ });
81
+ });
82
+
83
+ it("extracts example filters when --example appears after the filter flags", () => {
84
+ expect(
85
+ extractExampleRequest([
86
+ "node",
87
+ "zinkee",
88
+ "schemas",
89
+ "fields",
90
+ "create",
91
+ "contacts",
92
+ "--type",
93
+ "option",
94
+ "--example",
95
+ ]),
96
+ ).toEqual({
97
+ commandName: "schemas fields create",
98
+ filters: { type: "option" },
99
+ overrides: {},
100
+ json: false,
101
+ });
102
+ });
103
+
104
+ it("extracts example overrides regardless of --example position", () => {
105
+ expect(
106
+ extractExampleRequest([
107
+ "node",
108
+ "zinkee",
109
+ "schemas",
110
+ "list",
111
+ "--profile",
112
+ "staging",
113
+ "--example",
114
+ "--json",
115
+ ]),
116
+ ).toEqual({
117
+ commandName: "schemas list",
118
+ filters: {},
119
+ overrides: { profile: "staging" },
120
+ json: true,
121
+ });
122
+ });
123
+
72
124
  it("shows field examples filtered by type", async () => {
73
125
  let output = "";
74
126
 
package/src/program.ts CHANGED
@@ -40,7 +40,9 @@ export function buildProgram(options: BuildProgramOptions = {}): Command {
40
40
  .option("--api-key <token>", "override the configured API key")
41
41
  .option("--read-only", "run in read-only mode")
42
42
  .option("--example", "show examples for a command")
43
- .option("--json", "emit JSON output");
43
+ .option("--json", "emit JSON output")
44
+ .option("--install-completion [shell]", "install shell completion (bash, zsh, fish, powershell)")
45
+ .option("--show-completion [shell]", "print shell completion script for the current shell");
44
46
 
45
47
  registerProfilesCommands(program, options);
46
48
  registerAutomationsCommands(program, options);