zinkee 0.1.53 → 0.1.55
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 +78 -4
- package/dist/{chunk-IEW5RTJV.js → chunk-FIWBICYE.js} +7 -3
- package/dist/chunk-FIWBICYE.js.map +1 -0
- package/dist/{examples-SKT3O7KD.js → examples-UJZKITVW.js} +59 -3
- package/dist/examples-UJZKITVW.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +486 -31
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-IEW5RTJV.js.map +0 -1
- package/dist/examples-SKT3O7KD.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -16,8 +16,9 @@ import {
|
|
|
16
16
|
normalizeSchemaFieldCreatePayload,
|
|
17
17
|
normalizeSchemaFieldType,
|
|
18
18
|
normalizeSchemaFieldUpdatePayload,
|
|
19
|
-
printTable
|
|
20
|
-
|
|
19
|
+
printTable,
|
|
20
|
+
toCliError
|
|
21
|
+
} from "./chunk-FIWBICYE.js";
|
|
21
22
|
|
|
22
23
|
// src/cli-runner.ts
|
|
23
24
|
import { realpathSync } from "fs";
|
|
@@ -29,6 +30,7 @@ var HOISTABLE_GLOBAL_FLAGS = {
|
|
|
29
30
|
"--profile": "value",
|
|
30
31
|
"--base-url": "value",
|
|
31
32
|
"--api-key": "value",
|
|
33
|
+
"--workspace": "value",
|
|
32
34
|
"--read-only": "boolean",
|
|
33
35
|
"--json": "boolean"
|
|
34
36
|
};
|
|
@@ -250,7 +252,7 @@ ${list}`
|
|
|
250
252
|
};
|
|
251
253
|
}
|
|
252
254
|
async function showExamples(commandName, json, stdout, filters = {}) {
|
|
253
|
-
const { getCommandExamples } = await import("./examples-
|
|
255
|
+
const { getCommandExamples } = await import("./examples-UJZKITVW.js");
|
|
254
256
|
const examples = getCommandExamples(commandName, filters);
|
|
255
257
|
if (examples.length === 0) {
|
|
256
258
|
throw new CliError("invalid_cli_usage", `No examples registered for "${commandName}".`);
|
|
@@ -290,7 +292,7 @@ import { createRequire } from "module";
|
|
|
290
292
|
import { Command as Command4 } from "commander";
|
|
291
293
|
|
|
292
294
|
// src/config.ts
|
|
293
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
295
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
294
296
|
import { homedir } from "os";
|
|
295
297
|
import { dirname, join } from "path";
|
|
296
298
|
import { parseTOML, stringifyTOML } from "confbox/toml";
|
|
@@ -299,7 +301,22 @@ var redactedSecretMask = "********";
|
|
|
299
301
|
var ENV_API_KEY = "ZINKEE_API_KEY";
|
|
300
302
|
var ENV_BASE_URL = "ZINKEE_API_BASE_URL";
|
|
301
303
|
var ENV_PROFILE = "ZINKEE_PROFILE";
|
|
304
|
+
var ENV_WORKSPACE = "ZINKEE_WORKSPACE";
|
|
302
305
|
var DEFAULT_PRODUCTION_BASE_URL = "https://api-z2.zinkee.com";
|
|
306
|
+
var DEFAULT_PRODUCTION_BACKEND_URL = "https://backend-z2.zinkee.com";
|
|
307
|
+
var SESSION_PROFILE_TYPE = "session";
|
|
308
|
+
var DEFAULT_SESSION_PROFILE = "superadmin";
|
|
309
|
+
var profileKeysByTomlKey = /* @__PURE__ */ new Map([
|
|
310
|
+
["type", "type"],
|
|
311
|
+
["api_key", "apiKey"],
|
|
312
|
+
["base_url", "baseUrl"],
|
|
313
|
+
["backend_url", "backendUrl"],
|
|
314
|
+
["email", "email"],
|
|
315
|
+
["session_token", "sessionToken"]
|
|
316
|
+
]);
|
|
317
|
+
var tomlKeysByProfileKey = new Map(
|
|
318
|
+
[...profileKeysByTomlKey].map(([tomlKey, profileKey]) => [profileKey, tomlKey])
|
|
319
|
+
);
|
|
303
320
|
var ProfileResolutionError = class extends Error {
|
|
304
321
|
name = "ProfileResolutionError";
|
|
305
322
|
constructor(message) {
|
|
@@ -322,12 +339,9 @@ function parseProfileConfig(value) {
|
|
|
322
339
|
return profile;
|
|
323
340
|
}
|
|
324
341
|
for (const [key, rawValue] of Object.entries(value)) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
if (key === "base_url") {
|
|
330
|
-
profile.baseUrl = toStringValue(rawValue);
|
|
342
|
+
const profileKey = profileKeysByTomlKey.get(key);
|
|
343
|
+
if (profileKey) {
|
|
344
|
+
profile[profileKey] = toStringValue(rawValue);
|
|
331
345
|
continue;
|
|
332
346
|
}
|
|
333
347
|
profile[key] = rawValue;
|
|
@@ -380,15 +394,7 @@ function serializeProfileConfig(profile) {
|
|
|
380
394
|
if (value === void 0) {
|
|
381
395
|
continue;
|
|
382
396
|
}
|
|
383
|
-
|
|
384
|
-
serialized.api_key = value;
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
if (key === "baseUrl") {
|
|
388
|
-
serialized.base_url = value;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
serialized[key] = value;
|
|
397
|
+
serialized[tomlKeysByProfileKey.get(key) ?? key] = value;
|
|
392
398
|
}
|
|
393
399
|
return serialized;
|
|
394
400
|
}
|
|
@@ -413,7 +419,8 @@ function serializeConfig(config) {
|
|
|
413
419
|
}
|
|
414
420
|
function writeConfig(config, filePath = defaultConfigPath) {
|
|
415
421
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
416
|
-
writeFileSync(filePath, serializeConfig(config), "utf8");
|
|
422
|
+
writeFileSync(filePath, serializeConfig(config), { encoding: "utf8", mode: 384 });
|
|
423
|
+
chmodSync(filePath, 384);
|
|
417
424
|
}
|
|
418
425
|
function setProfileConfig(config, profileName, updates) {
|
|
419
426
|
return {
|
|
@@ -454,9 +461,19 @@ function redactSecret(value) {
|
|
|
454
461
|
function redactProfile(profile) {
|
|
455
462
|
return {
|
|
456
463
|
...profile,
|
|
457
|
-
apiKey: redactSecret(profile.apiKey)
|
|
464
|
+
apiKey: redactSecret(profile.apiKey),
|
|
465
|
+
sessionToken: redactSecret(profile.sessionToken)
|
|
458
466
|
};
|
|
459
467
|
}
|
|
468
|
+
function isSessionProfile(profile) {
|
|
469
|
+
return profile?.type === SESSION_PROFILE_TYPE;
|
|
470
|
+
}
|
|
471
|
+
function describeProfileCredential(redactedProfile) {
|
|
472
|
+
if (isSessionProfile(redactedProfile)) {
|
|
473
|
+
return redactedProfile.email ? `session ${redactedProfile.email}` : "session";
|
|
474
|
+
}
|
|
475
|
+
return redactedProfile.apiKey ?? "";
|
|
476
|
+
}
|
|
460
477
|
function redactConfig(config) {
|
|
461
478
|
return {
|
|
462
479
|
...config,
|
|
@@ -507,6 +524,9 @@ function readEnvBaseUrl(env = process.env) {
|
|
|
507
524
|
function readEnvProfile(env = process.env) {
|
|
508
525
|
return toNonEmptyString(env[ENV_PROFILE]);
|
|
509
526
|
}
|
|
527
|
+
function readEnvWorkspace(env = process.env) {
|
|
528
|
+
return toNonEmptyString(env[ENV_WORKSPACE]);
|
|
529
|
+
}
|
|
510
530
|
|
|
511
531
|
// src/commands/shared.ts
|
|
512
532
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -531,7 +551,196 @@ function normalizeUuidOrSlug(input) {
|
|
|
531
551
|
};
|
|
532
552
|
}
|
|
533
553
|
|
|
554
|
+
// src/session.ts
|
|
555
|
+
var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
556
|
+
var MAX_WORKSPACE_SUGGESTIONS = 5;
|
|
557
|
+
var WORKSPACES_LIST_ACTION = "Run `zinkee workspaces list` to see the available workspaces.";
|
|
558
|
+
function decodeTokenExpiry(token) {
|
|
559
|
+
const payload = token.split(".")[1];
|
|
560
|
+
if (!payload) {
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
try {
|
|
564
|
+
const { exp } = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
565
|
+
return typeof exp === "number" ? new Date(exp * 1e3) : void 0;
|
|
566
|
+
} catch {
|
|
567
|
+
return void 0;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
function isTokenExpired(token, now = Date.now()) {
|
|
571
|
+
const expiry = decodeTokenExpiry(token);
|
|
572
|
+
return expiry !== void 0 && expiry.getTime() <= now;
|
|
573
|
+
}
|
|
574
|
+
function sessionRequiredError(profile) {
|
|
575
|
+
const action = `Run \`zinkee login --profile ${profile}\`.`;
|
|
576
|
+
return new CliError(
|
|
577
|
+
"authentication_required",
|
|
578
|
+
`Session for profile "${profile}" has expired or is missing. ${action}`,
|
|
579
|
+
{ profile, action }
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
function requireSession(profile) {
|
|
583
|
+
const token = profile.sessionToken;
|
|
584
|
+
if (!token || isTokenExpired(token)) {
|
|
585
|
+
throw sessionRequiredError(profile.name);
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
profile: profile.name,
|
|
589
|
+
backendUrl: profile.backendUrl ?? DEFAULT_PRODUCTION_BACKEND_URL,
|
|
590
|
+
token
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function isHttpStatus(error, status) {
|
|
594
|
+
return error instanceof ApiClientError && error.status === status;
|
|
595
|
+
}
|
|
596
|
+
async function callBackend(session, call) {
|
|
597
|
+
try {
|
|
598
|
+
return await call(new ApiClient({ baseUrl: session.backendUrl, apiKey: session.token }));
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (isHttpStatus(error, 401)) {
|
|
601
|
+
throw sessionRequiredError(session.profile);
|
|
602
|
+
}
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function signIn(backendUrl, email, password) {
|
|
607
|
+
let response;
|
|
608
|
+
try {
|
|
609
|
+
response = await new ApiClient({ baseUrl: backendUrl }).requestJson("/member/sign-in", {
|
|
610
|
+
method: "POST",
|
|
611
|
+
headers: { "content-type": "application/json" },
|
|
612
|
+
body: JSON.stringify({ email, password })
|
|
613
|
+
});
|
|
614
|
+
} catch (error) {
|
|
615
|
+
if (isHttpStatus(error, 401)) {
|
|
616
|
+
throw new CliError(
|
|
617
|
+
"authentication_required",
|
|
618
|
+
"Sign-in failed: invalid email or password.",
|
|
619
|
+
toCliError(error).details
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
throw error;
|
|
623
|
+
}
|
|
624
|
+
if (typeof response?.token !== "string" || response.token.length === 0) {
|
|
625
|
+
throw new CliError("invalid_json", "Sign-in response did not include a session token.");
|
|
626
|
+
}
|
|
627
|
+
return response.token;
|
|
628
|
+
}
|
|
629
|
+
async function listWorkspaces(session) {
|
|
630
|
+
const organizations = await callBackend(
|
|
631
|
+
session,
|
|
632
|
+
(client) => client.getJson("/org/list")
|
|
633
|
+
);
|
|
634
|
+
return (organizations ?? []).flatMap(
|
|
635
|
+
(organization) => (organization.workspaces ?? []).map((workspace) => ({
|
|
636
|
+
organizationId: organization.id,
|
|
637
|
+
organizationName: organization.name,
|
|
638
|
+
workspaceId: workspace.id,
|
|
639
|
+
workspaceName: workspace.name,
|
|
640
|
+
publicName: workspace.publicName ?? void 0,
|
|
641
|
+
active: workspace.active !== false
|
|
642
|
+
}))
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
function issueWorkspaceToken(session, workspaceName) {
|
|
646
|
+
return callBackend(session, async (client) => {
|
|
647
|
+
const response = await client.requestJson(
|
|
648
|
+
`/${encodeURIComponent(workspaceName)}/settings/cli/runtime-token`,
|
|
649
|
+
{ method: "POST" }
|
|
650
|
+
);
|
|
651
|
+
if (typeof response?.token !== "string" || response.token.length === 0) {
|
|
652
|
+
throw new CliError(
|
|
653
|
+
"invalid_json",
|
|
654
|
+
`Workspace token response for "${workspaceName}" did not include a token.`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return { ...response, token: response.token, workspaceName: response.workspaceName ?? workspaceName };
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
function requireActive(workspace, requested) {
|
|
661
|
+
if (!workspace.active) {
|
|
662
|
+
throw new CliError(
|
|
663
|
+
"configuration_error",
|
|
664
|
+
`Workspace "${workspace.workspaceName}" is inactive.`,
|
|
665
|
+
{ workspace: requested, workspaceId: workspace.workspaceId }
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
return workspace;
|
|
669
|
+
}
|
|
670
|
+
function findWorkspaceById(workspaces, id) {
|
|
671
|
+
const match = workspaces.find((workspace) => workspace.workspaceId.toLowerCase() === id.toLowerCase());
|
|
672
|
+
if (!match) {
|
|
673
|
+
throw new CliError("configuration_error", `Workspace "${id}" not found.`, {
|
|
674
|
+
workspace: id,
|
|
675
|
+
action: WORKSPACES_LIST_ACTION
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
return requireActive(match, id);
|
|
679
|
+
}
|
|
680
|
+
function findWorkspaceByName(workspaces, name) {
|
|
681
|
+
const needle = name.toLowerCase();
|
|
682
|
+
const names = (workspace) => [workspace.workspaceName, workspace.publicName].filter((value) => Boolean(value));
|
|
683
|
+
const candidates = workspaces.filter(
|
|
684
|
+
(workspace) => names(workspace).some((value) => value.toLowerCase() === needle)
|
|
685
|
+
);
|
|
686
|
+
if (candidates.length === 0) {
|
|
687
|
+
const suggestions = [
|
|
688
|
+
...new Set(
|
|
689
|
+
workspaces.filter((workspace) => names(workspace).some((value) => value.toLowerCase().includes(needle))).map((workspace) => workspace.workspaceName)
|
|
690
|
+
)
|
|
691
|
+
].slice(0, MAX_WORKSPACE_SUGGESTIONS);
|
|
692
|
+
const hint = suggestions.length > 0 ? ` Did you mean ${suggestions.map((value) => `"${value}"`).join(", ")}?` : "";
|
|
693
|
+
throw new CliError("configuration_error", `Workspace "${name}" not found.${hint}`, {
|
|
694
|
+
workspace: name,
|
|
695
|
+
...suggestions.length > 0 ? { suggestions } : {},
|
|
696
|
+
action: WORKSPACES_LIST_ACTION
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const exact = candidates.find((workspace) => workspace.workspaceName === name);
|
|
700
|
+
if (exact) {
|
|
701
|
+
return requireActive(exact, name);
|
|
702
|
+
}
|
|
703
|
+
const active = candidates.filter((workspace) => workspace.active);
|
|
704
|
+
if (active.length === 1) {
|
|
705
|
+
return active[0];
|
|
706
|
+
}
|
|
707
|
+
if (active.length === 0) {
|
|
708
|
+
return requireActive(candidates[0], name);
|
|
709
|
+
}
|
|
710
|
+
const matches = active.map((workspace) => workspace.workspaceName);
|
|
711
|
+
throw new CliError(
|
|
712
|
+
"configuration_error",
|
|
713
|
+
`Workspace "${name}" matches several workspaces: ${matches.map((value) => `"${value}"`).join(", ")}. Pass the exact name or the workspace id.`,
|
|
714
|
+
{ workspace: name, candidates: matches }
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
async function exchangeWorkspaceToken(session, workspace) {
|
|
718
|
+
const requested = workspace.trim();
|
|
719
|
+
if (UUID_PATTERN.test(requested)) {
|
|
720
|
+
const match = findWorkspaceById(await listWorkspaces(session), requested);
|
|
721
|
+
return issueWorkspaceToken(session, match.workspaceName);
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
return await issueWorkspaceToken(session, requested);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
if (!isHttpStatus(error, 410)) {
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
const match = findWorkspaceByName(await listWorkspaces(session), requested);
|
|
730
|
+
if (match.workspaceName === requested) {
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
return issueWorkspaceToken(session, match.workspaceName);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
function createWorkspaceTokenProvider(session, workspace) {
|
|
737
|
+
return async () => (await exchangeWorkspaceToken(session, workspace)).token;
|
|
738
|
+
}
|
|
739
|
+
|
|
534
740
|
// src/runtime-context.ts
|
|
741
|
+
function workspaceNeedsSession(source) {
|
|
742
|
+
return `${source} only works with a session profile (run \`zinkee login\`).`;
|
|
743
|
+
}
|
|
535
744
|
function resolveRuntimeContext(config, overrides = {}) {
|
|
536
745
|
const envProfile = readEnvProfile();
|
|
537
746
|
const envApiKey = readEnvApiKey();
|
|
@@ -540,6 +749,8 @@ function resolveRuntimeContext(config, overrides = {}) {
|
|
|
540
749
|
const profileOverride = overrides.profile ?? envProfile;
|
|
541
750
|
const apiKeyOverride = overrides.apiKey ?? envApiKey;
|
|
542
751
|
const baseUrlOverride = flagBaseUrl ?? envBaseUrl;
|
|
752
|
+
const selectedProfileName = profileOverride ?? config.defaultProfile;
|
|
753
|
+
const selectsSessionProfile = selectedProfileName !== void 0 && isSessionProfile(config.profiles[selectedProfileName]);
|
|
543
754
|
if (apiKeyOverride && !baseUrlOverride) {
|
|
544
755
|
throw new ProfileResolutionError(
|
|
545
756
|
`Runtime API key was provided, but ${ENV_BASE_URL} or --base-url is missing.`
|
|
@@ -547,10 +758,15 @@ function resolveRuntimeContext(config, overrides = {}) {
|
|
|
547
758
|
}
|
|
548
759
|
if (baseUrlOverride && !apiKeyOverride) {
|
|
549
760
|
throw new ProfileResolutionError(
|
|
550
|
-
`Runtime base URL was provided, but ${ENV_API_KEY} or --api-key is missing.`
|
|
761
|
+
selectsSessionProfile ? `Session profile "${selectedProfileName}" takes its URLs from \`zinkee login\`: drop ${ENV_BASE_URL} or --base-url, or log in again with --backend-url and --base-url.` : `Runtime base URL was provided, but ${ENV_API_KEY} or --api-key is missing.`
|
|
551
762
|
);
|
|
552
763
|
}
|
|
553
764
|
if (apiKeyOverride && baseUrlOverride) {
|
|
765
|
+
if (overrides.workspace !== void 0) {
|
|
766
|
+
throw new ProfileResolutionError(
|
|
767
|
+
`${workspaceNeedsSession("--workspace")} It cannot be combined with a runtime API key.`
|
|
768
|
+
);
|
|
769
|
+
}
|
|
554
770
|
return {
|
|
555
771
|
profile: profileOverride ?? "runtime",
|
|
556
772
|
baseUrl: baseUrlOverride,
|
|
@@ -564,10 +780,31 @@ function resolveRuntimeContext(config, overrides = {}) {
|
|
|
564
780
|
baseUrl: baseUrlOverride,
|
|
565
781
|
apiKey: apiKeyOverride
|
|
566
782
|
});
|
|
783
|
+
const flagWorkspace = overrides.workspace?.trim() || void 0;
|
|
784
|
+
const workspace = flagWorkspace ?? readEnvWorkspace();
|
|
785
|
+
if (!isSessionProfile(profile)) {
|
|
786
|
+
if (workspace) {
|
|
787
|
+
throw new ProfileResolutionError(workspaceNeedsSession(flagWorkspace ? "--workspace" : ENV_WORKSPACE));
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
profile: profile.name,
|
|
791
|
+
baseUrl: baseUrlOverride ?? profile.baseUrl ?? DEFAULT_PRODUCTION_BASE_URL,
|
|
792
|
+
apiKey: apiKeyOverride ?? profile.apiKey,
|
|
793
|
+
readOnly: overrides.readOnly ?? false,
|
|
794
|
+
json: overrides.json ?? false
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (!workspace) {
|
|
798
|
+
throw new ProfileResolutionError(
|
|
799
|
+
`Profile "${profile.name}" is a session profile: pass --workspace <name> or set ${ENV_WORKSPACE}.`
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
const { backendUrl, token } = requireSession(profile);
|
|
567
803
|
return {
|
|
568
804
|
profile: profile.name,
|
|
569
|
-
baseUrl:
|
|
570
|
-
|
|
805
|
+
baseUrl: profile.baseUrl ?? DEFAULT_PRODUCTION_BASE_URL,
|
|
806
|
+
workspace,
|
|
807
|
+
session: { backendUrl, token },
|
|
571
808
|
readOnly: overrides.readOnly ?? false,
|
|
572
809
|
json: overrides.json ?? false
|
|
573
810
|
};
|
|
@@ -584,8 +821,9 @@ function createCommandContext(command, filePath) {
|
|
|
584
821
|
`Resolved profile "${runtime.profile}" is missing a base URL.`
|
|
585
822
|
);
|
|
586
823
|
}
|
|
824
|
+
const getToken = runtime.session && runtime.workspace ? createWorkspaceTokenProvider({ profile: runtime.profile, ...runtime.session }, runtime.workspace) : void 0;
|
|
587
825
|
return {
|
|
588
|
-
client: new ApiClient({ baseUrl: runtime.baseUrl, apiKey: runtime.apiKey }),
|
|
826
|
+
client: new ApiClient({ baseUrl: runtime.baseUrl, apiKey: runtime.apiKey, getToken }),
|
|
589
827
|
json: runtime.json,
|
|
590
828
|
profile: runtime.profile,
|
|
591
829
|
readOnly: runtime.readOnly
|
|
@@ -746,9 +984,9 @@ function registerConfigCommands(program, options = {}) {
|
|
|
746
984
|
name,
|
|
747
985
|
default: data.defaultProfile === name ? "yes" : "",
|
|
748
986
|
baseUrl: typeof profile.baseUrl === "string" ? profile.baseUrl : "",
|
|
749
|
-
|
|
987
|
+
credential: describeProfileCredential(profile)
|
|
750
988
|
})),
|
|
751
|
-
["name", "default", "baseUrl", "
|
|
989
|
+
["name", "default", "baseUrl", "credential"],
|
|
752
990
|
stdout
|
|
753
991
|
);
|
|
754
992
|
} catch (error) {
|
|
@@ -3592,6 +3830,9 @@ async function downloadToOutput(download, stdout, outputPath) {
|
|
|
3592
3830
|
await rename(tempFile, outputPath);
|
|
3593
3831
|
return bytes;
|
|
3594
3832
|
} catch (error) {
|
|
3833
|
+
if (error instanceof CliError) {
|
|
3834
|
+
throw error;
|
|
3835
|
+
}
|
|
3595
3836
|
const cause = error instanceof ApiClientError ? error.cause : error;
|
|
3596
3837
|
const isFileError = cause instanceof Error && "syscall" in cause && ["open", "write", "writev", "close"].includes(String(cause.syscall));
|
|
3597
3838
|
if (error instanceof ApiClientError && !isFileError) {
|
|
@@ -4351,9 +4592,9 @@ function emitProfilesTable(stdout, data) {
|
|
|
4351
4592
|
name: profile.name,
|
|
4352
4593
|
default: profile.isDefault ? "yes" : "",
|
|
4353
4594
|
baseUrl: profile.baseUrl ?? "",
|
|
4354
|
-
|
|
4595
|
+
credential: describeProfileCredential(profile)
|
|
4355
4596
|
})),
|
|
4356
|
-
["name", "default", "baseUrl", "
|
|
4597
|
+
["name", "default", "baseUrl", "credential"],
|
|
4357
4598
|
stdout
|
|
4358
4599
|
);
|
|
4359
4600
|
}
|
|
@@ -4433,7 +4674,13 @@ function registerProfilesCommands(program, options = {}) {
|
|
|
4433
4674
|
);
|
|
4434
4675
|
}
|
|
4435
4676
|
const config = readRequiredConfig(filePath);
|
|
4436
|
-
getRequiredProfile(config, profileName);
|
|
4677
|
+
const profile = getRequiredProfile(config, profileName);
|
|
4678
|
+
if (isSessionProfile(profile)) {
|
|
4679
|
+
throw new CliError(
|
|
4680
|
+
"configuration_error",
|
|
4681
|
+
`Profile "${profileName}" is a session profile. Run \`zinkee login --profile ${profileName}\` to renew its session, adding --backend-url and --base-url to change its environment.`
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4437
4684
|
const baseUrl = normalizeProfileBaseUrl(commandOptions.baseUrl);
|
|
4438
4685
|
const nextConfig = setProfileConfig(config, profileName, {
|
|
4439
4686
|
apiKey: commandOptions.apiKey,
|
|
@@ -5459,6 +5706,213 @@ function registerSchemasCommands(program, options = {}) {
|
|
|
5459
5706
|
});
|
|
5460
5707
|
}
|
|
5461
5708
|
|
|
5709
|
+
// src/commands/session.ts
|
|
5710
|
+
import { createInterface } from "readline/promises";
|
|
5711
|
+
import { Writable as Writable2 } from "stream";
|
|
5712
|
+
function readConfigOrEmpty(filePath) {
|
|
5713
|
+
try {
|
|
5714
|
+
return readConfigIfExists(filePath) ?? { profiles: {} };
|
|
5715
|
+
} catch (error) {
|
|
5716
|
+
toConfigurationError(filePath, error);
|
|
5717
|
+
}
|
|
5718
|
+
}
|
|
5719
|
+
function normalizeUrl(value, source) {
|
|
5720
|
+
try {
|
|
5721
|
+
return validateBaseUrl(value, source);
|
|
5722
|
+
} catch (error) {
|
|
5723
|
+
throw new CliError("configuration_error", error instanceof Error ? error.message : String(error));
|
|
5724
|
+
}
|
|
5725
|
+
}
|
|
5726
|
+
function resolveLoginUrls(options, existing) {
|
|
5727
|
+
const backendFlag = options.backendUrl === void 0 ? void 0 : normalizeUrl(options.backendUrl, '"--backend-url"');
|
|
5728
|
+
const baseFlag = options.baseUrl === void 0 ? void 0 : normalizeUrl(options.baseUrl, '"--base-url"');
|
|
5729
|
+
if (backendFlag === void 0 && baseFlag === void 0) {
|
|
5730
|
+
return {
|
|
5731
|
+
backendUrl: existing?.backendUrl ?? DEFAULT_PRODUCTION_BACKEND_URL,
|
|
5732
|
+
baseUrl: existing?.baseUrl ?? DEFAULT_PRODUCTION_BASE_URL
|
|
5733
|
+
};
|
|
5734
|
+
}
|
|
5735
|
+
if (backendFlag === void 0 || baseFlag === void 0) {
|
|
5736
|
+
throw new CliError(
|
|
5737
|
+
"invalid_cli_usage",
|
|
5738
|
+
'Pass both "--backend-url" and "--base-url": a session only works with the Public API of the same environment.'
|
|
5739
|
+
);
|
|
5740
|
+
}
|
|
5741
|
+
return { backendUrl: backendFlag, baseUrl: baseFlag };
|
|
5742
|
+
}
|
|
5743
|
+
async function prompt(input, label, hidden) {
|
|
5744
|
+
const output = new Writable2({
|
|
5745
|
+
write(chunk, _encoding, callback) {
|
|
5746
|
+
if (!hidden) {
|
|
5747
|
+
process.stderr.write(chunk);
|
|
5748
|
+
}
|
|
5749
|
+
callback();
|
|
5750
|
+
}
|
|
5751
|
+
});
|
|
5752
|
+
const rl = createInterface({ input, output, terminal: true, historySize: 0 });
|
|
5753
|
+
const controller = new AbortController();
|
|
5754
|
+
rl.on("SIGINT", () => controller.abort());
|
|
5755
|
+
rl.on("close", () => controller.abort());
|
|
5756
|
+
if (hidden) {
|
|
5757
|
+
process.stderr.write(label);
|
|
5758
|
+
}
|
|
5759
|
+
try {
|
|
5760
|
+
return await rl.question(hidden ? "" : label, { signal: controller.signal });
|
|
5761
|
+
} catch (error) {
|
|
5762
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
5763
|
+
throw new CliError("invalid_cli_usage", "Login cancelled.", {}, 130);
|
|
5764
|
+
}
|
|
5765
|
+
throw error;
|
|
5766
|
+
} finally {
|
|
5767
|
+
if (hidden) {
|
|
5768
|
+
process.stderr.write("\n");
|
|
5769
|
+
}
|
|
5770
|
+
rl.close();
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
async function readAll(input) {
|
|
5774
|
+
const chunks = [];
|
|
5775
|
+
for await (const chunk of input) {
|
|
5776
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
5777
|
+
}
|
|
5778
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
5779
|
+
}
|
|
5780
|
+
async function readEmail(options, existing, input) {
|
|
5781
|
+
const email = options.email?.trim() || existing?.email;
|
|
5782
|
+
if (email) {
|
|
5783
|
+
return email;
|
|
5784
|
+
}
|
|
5785
|
+
if (options.passwordStdin) {
|
|
5786
|
+
throw new CliError("invalid_cli_usage", 'Pass "--email" when using "--password-stdin".');
|
|
5787
|
+
}
|
|
5788
|
+
const answer = (await prompt(input, "Email: ", false)).trim();
|
|
5789
|
+
if (!answer) {
|
|
5790
|
+
throw new CliError("invalid_cli_usage", "An email is required to log in.");
|
|
5791
|
+
}
|
|
5792
|
+
return answer;
|
|
5793
|
+
}
|
|
5794
|
+
async function readPassword(options, input) {
|
|
5795
|
+
const password = options.passwordStdin ? (await readAll(input)).replace(/\r?\n$/, "") : await prompt(input, "Password: ", true);
|
|
5796
|
+
if (!password) {
|
|
5797
|
+
throw new CliError("invalid_cli_usage", "A password is required to log in.");
|
|
5798
|
+
}
|
|
5799
|
+
return password;
|
|
5800
|
+
}
|
|
5801
|
+
function resolveListingSession(filePath, options) {
|
|
5802
|
+
try {
|
|
5803
|
+
const profile = resolveProfile(readRuntimeConfig(filePath), {
|
|
5804
|
+
profile: options.profile ?? readEnvProfile()
|
|
5805
|
+
});
|
|
5806
|
+
if (!isSessionProfile(profile)) {
|
|
5807
|
+
throw new CliError(
|
|
5808
|
+
"configuration_error",
|
|
5809
|
+
`Profile "${profile.name}" is not a session profile. Run \`zinkee login\` or pass --profile with a session profile.`
|
|
5810
|
+
);
|
|
5811
|
+
}
|
|
5812
|
+
return requireSession(profile);
|
|
5813
|
+
} catch (error) {
|
|
5814
|
+
toConfigurationError(filePath, error);
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
function registerSessionCommands(program, options = {}) {
|
|
5818
|
+
const filePath = options.configPath ?? defaultConfigPath;
|
|
5819
|
+
const stdout = options.stdout ?? process.stdout;
|
|
5820
|
+
const stdin = options.stdin ?? process.stdin;
|
|
5821
|
+
program.command("login").description(
|
|
5822
|
+
`Sign in with a Zinkee user and store a session profile (default "${DEFAULT_SESSION_PROFILE}")`
|
|
5823
|
+
).option("--email <email>", "user email (defaults to the stored one, else prompts)").option("--backend-url <url>", `web backend URL (default ${DEFAULT_PRODUCTION_BACKEND_URL})`).option("--base-url <url>", `Public API URL (default ${DEFAULT_PRODUCTION_BASE_URL})`).option("--password-stdin", "read the password from stdin").action(async function action() {
|
|
5824
|
+
const loginOptions = this.optsWithGlobals();
|
|
5825
|
+
if (!loginOptions.passwordStdin && !stdin.isTTY) {
|
|
5826
|
+
throw new CliError(
|
|
5827
|
+
"invalid_cli_usage",
|
|
5828
|
+
'Stdin is not an interactive terminal: pass the password with "--password-stdin".'
|
|
5829
|
+
);
|
|
5830
|
+
}
|
|
5831
|
+
const profileName = loginOptions.profile ?? DEFAULT_SESSION_PROFILE;
|
|
5832
|
+
const config = readConfigOrEmpty(filePath);
|
|
5833
|
+
const existing = config.profiles[profileName];
|
|
5834
|
+
if (existing && !isSessionProfile(existing)) {
|
|
5835
|
+
throw new CliError(
|
|
5836
|
+
"configuration_error",
|
|
5837
|
+
`Profile "${profileName}" uses an API key. Choose another name with --profile.`
|
|
5838
|
+
);
|
|
5839
|
+
}
|
|
5840
|
+
const { backendUrl, baseUrl } = resolveLoginUrls(loginOptions, existing);
|
|
5841
|
+
const email = await readEmail(loginOptions, existing, stdin);
|
|
5842
|
+
const password = await readPassword(loginOptions, stdin);
|
|
5843
|
+
const token = await signIn(backendUrl, email, password);
|
|
5844
|
+
let nextConfig = setProfileConfig(config, profileName, {
|
|
5845
|
+
type: SESSION_PROFILE_TYPE,
|
|
5846
|
+
backendUrl,
|
|
5847
|
+
baseUrl,
|
|
5848
|
+
email,
|
|
5849
|
+
sessionToken: token
|
|
5850
|
+
});
|
|
5851
|
+
if (!nextConfig.defaultProfile) {
|
|
5852
|
+
nextConfig = setDefaultProfile(nextConfig, profileName);
|
|
5853
|
+
}
|
|
5854
|
+
writeConfig(nextConfig, filePath);
|
|
5855
|
+
const expiresAt = decodeTokenExpiry(token)?.toISOString() ?? null;
|
|
5856
|
+
if (loginOptions.json) {
|
|
5857
|
+
emitJson(stdout, "login", { profile: profileName, email, backendUrl, baseUrl, expiresAt }, filePath);
|
|
5858
|
+
return;
|
|
5859
|
+
}
|
|
5860
|
+
writeLine(
|
|
5861
|
+
stdout,
|
|
5862
|
+
expiresAt ? `Logged in as ${email} (profile ${profileName}, session valid until ${expiresAt}).` : `Logged in as ${email} (profile ${profileName}).`
|
|
5863
|
+
);
|
|
5864
|
+
});
|
|
5865
|
+
program.command("logout").description(`Remove the stored session of a session profile (default "${DEFAULT_SESSION_PROFILE}")`).action(function action() {
|
|
5866
|
+
const logoutOptions = this.optsWithGlobals();
|
|
5867
|
+
const profileName = logoutOptions.profile ?? DEFAULT_SESSION_PROFILE;
|
|
5868
|
+
const config = readConfigOrEmpty(filePath);
|
|
5869
|
+
const profile = config.profiles[profileName];
|
|
5870
|
+
if (!profile) {
|
|
5871
|
+
throw new CliError("configuration_error", `Profile "${profileName}" not found in config.`);
|
|
5872
|
+
}
|
|
5873
|
+
if (!isSessionProfile(profile)) {
|
|
5874
|
+
throw new CliError("configuration_error", `Profile "${profileName}" is not a session profile.`);
|
|
5875
|
+
}
|
|
5876
|
+
writeConfig(setProfileConfig(config, profileName, { sessionToken: void 0 }), filePath);
|
|
5877
|
+
if (logoutOptions.json) {
|
|
5878
|
+
emitJson(stdout, "logout", { profile: profileName, email: profile.email ?? null }, filePath);
|
|
5879
|
+
return;
|
|
5880
|
+
}
|
|
5881
|
+
writeLine(
|
|
5882
|
+
stdout,
|
|
5883
|
+
`Removed the session of profile "${profileName}" from this machine. The server cannot revoke it, so it stays valid until it expires.`
|
|
5884
|
+
);
|
|
5885
|
+
});
|
|
5886
|
+
const workspaces = program.command("workspaces").description("Browse organizations and workspaces with a session profile");
|
|
5887
|
+
workspaces.command("list").description("List every organization and workspace visible to the session").action(async function action() {
|
|
5888
|
+
const listOptions = this.optsWithGlobals();
|
|
5889
|
+
const session = resolveListingSession(filePath, listOptions);
|
|
5890
|
+
const data = (await listWorkspaces(session)).sort(
|
|
5891
|
+
(left, right) => left.organizationName.localeCompare(right.organizationName) || left.workspaceName.localeCompare(right.workspaceName)
|
|
5892
|
+
).map(({ organizationId, organizationName, workspaceId, workspaceName, active }) => ({
|
|
5893
|
+
organizationId,
|
|
5894
|
+
organizationName,
|
|
5895
|
+
workspaceId,
|
|
5896
|
+
workspaceName,
|
|
5897
|
+
active
|
|
5898
|
+
}));
|
|
5899
|
+
if (listOptions.json) {
|
|
5900
|
+
emitJson(stdout, "workspaces list", data);
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
printTable(
|
|
5904
|
+
data.map((row) => ({
|
|
5905
|
+
organization: row.organizationName,
|
|
5906
|
+
workspace: row.workspaceName,
|
|
5907
|
+
id: row.workspaceId,
|
|
5908
|
+
active: row.active ? "yes" : "no"
|
|
5909
|
+
})),
|
|
5910
|
+
["organization", "workspace", "id", "active"],
|
|
5911
|
+
stdout
|
|
5912
|
+
);
|
|
5913
|
+
});
|
|
5914
|
+
}
|
|
5915
|
+
|
|
5462
5916
|
// src/commands/teamspace.ts
|
|
5463
5917
|
import { readFileSync as readFileSync6 } from "fs";
|
|
5464
5918
|
|
|
@@ -5735,8 +6189,9 @@ var require2 = createRequire(import.meta.url);
|
|
|
5735
6189
|
var { version } = require2("../package.json");
|
|
5736
6190
|
function buildProgram(options = {}) {
|
|
5737
6191
|
const program = new Command4();
|
|
5738
|
-
program.name("zinkee").description("CLI for Zinkee API v2").version(version).enablePositionalOptions().showHelpAfterError().option("--profile <name>", "select a profile from config").option("--base-url <url>", "override the configured base URL").option("--api-key <token>", "override the configured API key").option("--read-only", "run in read-only mode").option("--example", "show examples for a command").option("--json", "emit JSON output").option("--install-completion [shell]", "install shell completion (bash, zsh, fish, powershell)").option("--show-completion [shell]", "print shell completion script for the current shell");
|
|
6192
|
+
program.name("zinkee").description("CLI for Zinkee API v2").version(version).enablePositionalOptions().showHelpAfterError().option("--profile <name>", "select a profile from config").option("--base-url <url>", "override the configured base URL").option("--api-key <token>", "override the configured API key").option("--workspace <workspace>", "workspace name or id for a session profile (or ZINKEE_WORKSPACE)").option("--read-only", "run in read-only mode").option("--example", "show examples for a command").option("--json", "emit JSON output").option("--install-completion [shell]", "install shell completion (bash, zsh, fish, powershell)").option("--show-completion [shell]", "print shell completion script for the current shell");
|
|
5739
6193
|
registerProfilesCommands(program, options);
|
|
6194
|
+
registerSessionCommands(program, options);
|
|
5740
6195
|
registerAutomationsCommands(program, options);
|
|
5741
6196
|
registerConfigCommands(program, options);
|
|
5742
6197
|
registerDisplaysCommands(program, options);
|