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
|
@@ -1,150 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
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/config.test.ts
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_PRODUCTION_BASE_URL,
|
|
5
|
-
ENV_API_KEY,
|
|
6
|
-
ENV_BASE_URL,
|
|
7
|
-
ENV_PROFILE,
|
|
8
|
-
parseConfig,
|
|
9
|
-
redactConfig,
|
|
10
|
-
redactSecret,
|
|
11
|
-
readRuntimeConfig,
|
|
12
|
-
readEnvApiKey,
|
|
13
|
-
readEnvBaseUrl,
|
|
14
|
-
readEnvProfile,
|
|
15
|
-
removeProfileConfig,
|
|
16
|
-
resolveProfile,
|
|
17
|
-
serializeConfig,
|
|
18
|
-
setDefaultProfile,
|
|
19
|
-
setProfileConfig,
|
|
20
|
-
} from "./config.js";
|
|
21
|
-
import { resolveRuntimeContext } from "./runtime-context.js";
|
|
22
|
-
|
|
23
|
-
describe("parseConfig", () => {
|
|
24
|
-
it("reads the legacy config shape without migration", () => {
|
|
25
|
-
const config = parseConfig(`
|
|
26
|
-
default_profile = "prod"
|
|
27
|
-
|
|
28
|
-
[profiles.prod]
|
|
29
|
-
api_key = "token"
|
|
30
|
-
base_url = "https://api.zinkee.com"
|
|
31
|
-
retry_count = 3
|
|
32
|
-
`);
|
|
33
|
-
|
|
34
|
-
expect(config.defaultProfile).toBe("prod");
|
|
35
|
-
expect(config.profiles.prod?.apiKey).toBe("token");
|
|
36
|
-
expect(config.profiles.prod?.baseUrl).toBe("https://api.zinkee.com");
|
|
37
|
-
expect(config.profiles.prod?.retry_count).toBe(3);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("readRuntimeConfig", () => {
|
|
42
|
-
it("returns an empty runtime config when the file does not exist", () => {
|
|
43
|
-
expect(readRuntimeConfig("/tmp/definitely-missing-zinkee-config.toml")).toEqual({
|
|
44
|
-
profiles: {},
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
describe("resolveProfile", () => {
|
|
50
|
-
it("prefers explicit profile over default_profile", () => {
|
|
51
|
-
const config = {
|
|
52
|
-
defaultProfile: "prod",
|
|
53
|
-
profiles: {
|
|
54
|
-
prod: { apiKey: "a" },
|
|
55
|
-
local: { apiKey: "b", baseUrl: "http://localhost:8088" },
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
expect(resolveProfile(config, { profile: "local" }).name).toBe("local");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("throws a clear error when the explicit profile is missing", () => {
|
|
63
|
-
const config = {
|
|
64
|
-
defaultProfile: "prod",
|
|
65
|
-
profiles: {
|
|
66
|
-
prod: { apiKey: "a" },
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
expect(() => resolveProfile(config, { profile: "local" })).toThrowError(
|
|
71
|
-
'Profile "local" not found in config.',
|
|
72
|
-
);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("throws a clear error when no effective profile exists", () => {
|
|
76
|
-
const config = {
|
|
77
|
-
profiles: {},
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
expect(() => resolveProfile(config)).toThrowError("No profile could be resolved from config.");
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe("resolveRuntimeContext", () => {
|
|
85
|
-
it("applies profile, baseUrl, apiKey, readOnly, and json overrides cleanly", () => {
|
|
86
|
-
const config = {
|
|
87
|
-
defaultProfile: "prod",
|
|
88
|
-
profiles: {
|
|
89
|
-
prod: { apiKey: "token", baseUrl: "https://api.zinkee.com" },
|
|
90
|
-
local: { apiKey: "local-token", baseUrl: "http://localhost:8088" },
|
|
91
|
-
},
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
expect(
|
|
95
|
-
resolveRuntimeContext(config, {
|
|
96
|
-
profile: "local",
|
|
97
|
-
baseUrl: "http://127.0.0.1:8088",
|
|
98
|
-
apiKey: "override-token",
|
|
99
|
-
readOnly: true,
|
|
100
|
-
json: false,
|
|
101
|
-
}),
|
|
102
|
-
).toEqual({
|
|
103
|
-
profile: "local",
|
|
104
|
-
baseUrl: "http://127.0.0.1:8088",
|
|
105
|
-
apiKey: "override-token",
|
|
106
|
-
readOnly: true,
|
|
107
|
-
json: false,
|
|
108
|
-
profileSource: "flag",
|
|
109
|
-
baseUrlSource: "flag",
|
|
110
|
-
apiKeySource: "flag",
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("resolves runtime directly from env vars without config", () => {
|
|
115
|
-
const previousApiKey = process.env[ENV_API_KEY];
|
|
116
|
-
const previousBaseUrl = process.env[ENV_BASE_URL];
|
|
117
|
-
const previousProfile = process.env[ENV_PROFILE];
|
|
118
|
-
process.env[ENV_API_KEY] = "env-token";
|
|
119
|
-
process.env[ENV_BASE_URL] = "http://localhost:8088/";
|
|
120
|
-
delete process.env[ENV_PROFILE];
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
expect(resolveRuntimeContext({ profiles: {} }, {})).toEqual({
|
|
124
|
-
profile: "runtime",
|
|
125
|
-
baseUrl: "http://localhost:8088",
|
|
126
|
-
apiKey: "env-token",
|
|
127
|
-
readOnly: false,
|
|
128
|
-
json: false,
|
|
129
|
-
profileSource: undefined,
|
|
130
|
-
baseUrlSource: "env",
|
|
131
|
-
apiKeySource: "env",
|
|
132
|
-
});
|
|
133
|
-
} finally {
|
|
134
|
-
if (previousApiKey === undefined) delete process.env[ENV_API_KEY];
|
|
135
|
-
else process.env[ENV_API_KEY] = previousApiKey;
|
|
136
|
-
if (previousBaseUrl === undefined) delete process.env[ENV_BASE_URL];
|
|
137
|
-
else process.env[ENV_BASE_URL] = previousBaseUrl;
|
|
138
|
-
if (previousProfile === undefined) delete process.env[ENV_PROFILE];
|
|
139
|
-
else process.env[ENV_PROFILE] = previousProfile;
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("prefers env vars over config values", () => {
|
|
144
|
-
const previousApiKey = process.env[ENV_API_KEY];
|
|
145
|
-
const previousBaseUrl = process.env[ENV_BASE_URL];
|
|
146
|
-
const previousProfile = process.env[ENV_PROFILE];
|
|
147
|
-
process.env[ENV_API_KEY] = "env-token";
|
|
148
|
-
process.env[ENV_BASE_URL] = "http://localhost:8088";
|
|
149
|
-
process.env[ENV_PROFILE] = "local";
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
expect(
|
|
153
|
-
resolveRuntimeContext({
|
|
154
|
-
defaultProfile: "prod",
|
|
155
|
-
profiles: {
|
|
156
|
-
prod: { apiKey: "prod-token", baseUrl: "https://api.zinkee.com" },
|
|
157
|
-
local: { apiKey: "local-token", baseUrl: "http://127.0.0.1:9999" },
|
|
158
|
-
},
|
|
159
|
-
}, {}),
|
|
160
|
-
).toEqual({
|
|
161
|
-
profile: "local",
|
|
162
|
-
baseUrl: "http://localhost:8088",
|
|
163
|
-
apiKey: "env-token",
|
|
164
|
-
readOnly: false,
|
|
165
|
-
json: false,
|
|
166
|
-
profileSource: "env",
|
|
167
|
-
baseUrlSource: "env",
|
|
168
|
-
apiKeySource: "env",
|
|
169
|
-
});
|
|
170
|
-
} finally {
|
|
171
|
-
if (previousApiKey === undefined) delete process.env[ENV_API_KEY];
|
|
172
|
-
else process.env[ENV_API_KEY] = previousApiKey;
|
|
173
|
-
if (previousBaseUrl === undefined) delete process.env[ENV_BASE_URL];
|
|
174
|
-
else process.env[ENV_BASE_URL] = previousBaseUrl;
|
|
175
|
-
if (previousProfile === undefined) delete process.env[ENV_PROFILE];
|
|
176
|
-
else process.env[ENV_PROFILE] = previousProfile;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("falls back to the production base URL when the resolved profile has no base URL", () => {
|
|
181
|
-
expect(
|
|
182
|
-
resolveRuntimeContext({
|
|
183
|
-
defaultProfile: "prod",
|
|
184
|
-
profiles: {
|
|
185
|
-
prod: { apiKey: "prod-token" },
|
|
186
|
-
},
|
|
187
|
-
}, {}),
|
|
188
|
-
).toEqual({
|
|
189
|
-
profile: "prod",
|
|
190
|
-
baseUrl: DEFAULT_PRODUCTION_BASE_URL,
|
|
191
|
-
apiKey: "prod-token",
|
|
192
|
-
readOnly: false,
|
|
193
|
-
json: false,
|
|
194
|
-
profileSource: "config",
|
|
195
|
-
baseUrlSource: "config",
|
|
196
|
-
apiKeySource: "config",
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it("still prefers an explicit base URL override over the production fallback", () => {
|
|
201
|
-
expect(
|
|
202
|
-
resolveRuntimeContext({
|
|
203
|
-
defaultProfile: "prod",
|
|
204
|
-
profiles: {
|
|
205
|
-
prod: { apiKey: "prod-token" },
|
|
206
|
-
},
|
|
207
|
-
}, {
|
|
208
|
-
profile: "prod",
|
|
209
|
-
baseUrl: "http://localhost:8088",
|
|
210
|
-
apiKey: "override-token",
|
|
211
|
-
}),
|
|
212
|
-
).toEqual({
|
|
213
|
-
profile: "prod",
|
|
214
|
-
baseUrl: "http://localhost:8088",
|
|
215
|
-
apiKey: "override-token",
|
|
216
|
-
readOnly: false,
|
|
217
|
-
json: false,
|
|
218
|
-
profileSource: "flag",
|
|
219
|
-
baseUrlSource: "flag",
|
|
220
|
-
apiKeySource: "flag",
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it("throws a clear error when runtime api key is provided without base url", () => {
|
|
225
|
-
const previousApiKey = process.env[ENV_API_KEY];
|
|
226
|
-
const previousBaseUrl = process.env[ENV_BASE_URL];
|
|
227
|
-
process.env[ENV_API_KEY] = "env-token";
|
|
228
|
-
delete process.env[ENV_BASE_URL];
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
expect(() => resolveRuntimeContext({ profiles: {} }, {})).toThrowError(
|
|
232
|
-
`Runtime API key was provided, but ${ENV_BASE_URL} or --base-url is missing.`,
|
|
233
|
-
);
|
|
234
|
-
} finally {
|
|
235
|
-
if (previousApiKey === undefined) delete process.env[ENV_API_KEY];
|
|
236
|
-
else process.env[ENV_API_KEY] = previousApiKey;
|
|
237
|
-
if (previousBaseUrl === undefined) delete process.env[ENV_BASE_URL];
|
|
238
|
-
else process.env[ENV_BASE_URL] = previousBaseUrl;
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("throws a clear error when runtime base url is provided without api key", () => {
|
|
243
|
-
const previousApiKey = process.env[ENV_API_KEY];
|
|
244
|
-
const previousBaseUrl = process.env[ENV_BASE_URL];
|
|
245
|
-
delete process.env[ENV_API_KEY];
|
|
246
|
-
process.env[ENV_BASE_URL] = "http://localhost:8088";
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
expect(() => resolveRuntimeContext({ profiles: {} }, {})).toThrowError(
|
|
250
|
-
`Runtime base URL was provided, but ${ENV_API_KEY} or --api-key is missing.`,
|
|
251
|
-
);
|
|
252
|
-
} finally {
|
|
253
|
-
if (previousApiKey === undefined) delete process.env[ENV_API_KEY];
|
|
254
|
-
else process.env[ENV_API_KEY] = previousApiKey;
|
|
255
|
-
if (previousBaseUrl === undefined) delete process.env[ENV_BASE_URL];
|
|
256
|
-
else process.env[ENV_BASE_URL] = previousBaseUrl;
|
|
257
|
-
}
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("rejects a --base-url override that is not an absolute http(s) URL", () => {
|
|
261
|
-
expect(() =>
|
|
262
|
-
resolveRuntimeContext({ profiles: {} }, { baseUrl: "api-z2.zinkee.com", apiKey: "k" }),
|
|
263
|
-
).toThrowError('"--base-url" must be an absolute http(s) URL.');
|
|
264
|
-
|
|
265
|
-
expect(() =>
|
|
266
|
-
resolveRuntimeContext(
|
|
267
|
-
{ profiles: {} },
|
|
268
|
-
{ baseUrl: "ftp://api-z2.zinkee.com", apiKey: "k" },
|
|
269
|
-
),
|
|
270
|
-
).toThrowError('"--base-url" must be an absolute http(s) URL.');
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it("normalizes trailing slashes on a --base-url override", () => {
|
|
274
|
-
expect(
|
|
275
|
-
resolveRuntimeContext(
|
|
276
|
-
{ profiles: {} },
|
|
277
|
-
{ baseUrl: "https://api-z2.zinkee.com///", apiKey: "k" },
|
|
278
|
-
).baseUrl,
|
|
279
|
-
).toBe("https://api-z2.zinkee.com");
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe("env readers", () => {
|
|
284
|
-
it("normalizes env values", () => {
|
|
285
|
-
expect(readEnvApiKey({ [ENV_API_KEY]: " token " } as NodeJS.ProcessEnv)).toBe("token");
|
|
286
|
-
expect(readEnvProfile({ [ENV_PROFILE]: " local " } as NodeJS.ProcessEnv)).toBe("local");
|
|
287
|
-
expect(readEnvBaseUrl({ [ENV_BASE_URL]: " http://localhost:8088/ " } as NodeJS.ProcessEnv)).toBe(
|
|
288
|
-
"http://localhost:8088",
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe("config mutation helpers", () => {
|
|
294
|
-
it("serializes updates back to the legacy TOML shape", () => {
|
|
295
|
-
const nextConfig = setDefaultProfile(
|
|
296
|
-
setProfileConfig(
|
|
297
|
-
{
|
|
298
|
-
profiles: {},
|
|
299
|
-
},
|
|
300
|
-
"local",
|
|
301
|
-
{
|
|
302
|
-
apiKey: "token",
|
|
303
|
-
baseUrl: "http://localhost:8088",
|
|
304
|
-
},
|
|
305
|
-
),
|
|
306
|
-
"local",
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
expect(serializeConfig(nextConfig)).toContain('default_profile = "local"');
|
|
310
|
-
expect(serializeConfig(nextConfig)).toContain('[profiles.local]');
|
|
311
|
-
expect(serializeConfig(nextConfig)).toContain('api_key = "token"');
|
|
312
|
-
expect(serializeConfig(nextConfig)).toContain('base_url = "http://localhost:8088"');
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("removes a deleted default profile cleanly", () => {
|
|
316
|
-
const config = {
|
|
317
|
-
defaultProfile: "prod",
|
|
318
|
-
profiles: {
|
|
319
|
-
prod: { apiKey: "token" },
|
|
320
|
-
local: { apiKey: "other" },
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
expect(removeProfileConfig(config, "prod")).toEqual({
|
|
325
|
-
defaultProfile: undefined,
|
|
326
|
-
profiles: {
|
|
327
|
-
local: { apiKey: "other" },
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
describe("redaction helpers", () => {
|
|
334
|
-
it("redacts secrets while keeping the last four characters", () => {
|
|
335
|
-
expect(redactSecret("secret-token-1234")).toBe("********1234");
|
|
336
|
-
expect(redactSecret("abc")).toBe("********");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
it("redacts config output without mutating other fields", () => {
|
|
340
|
-
expect(
|
|
341
|
-
redactConfig({
|
|
342
|
-
defaultProfile: "prod",
|
|
343
|
-
profiles: {
|
|
344
|
-
prod: {
|
|
345
|
-
apiKey: "secret-token-1234",
|
|
346
|
-
baseUrl: "https://api.zinkee.test",
|
|
347
|
-
retry_count: 3,
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
}),
|
|
351
|
-
).toEqual({
|
|
352
|
-
defaultProfile: "prod",
|
|
353
|
-
profiles: {
|
|
354
|
-
prod: {
|
|
355
|
-
apiKey: "********1234",
|
|
356
|
-
baseUrl: "https://api.zinkee.test",
|
|
357
|
-
retry_count: 3,
|
|
358
|
-
},
|
|
359
|
-
},
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
});
|