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/dist/index.js CHANGED
@@ -16,8 +16,9 @@ import {
16
16
  normalizeSchemaFieldCreatePayload,
17
17
  normalizeSchemaFieldType,
18
18
  normalizeSchemaFieldUpdatePayload,
19
- printTable
20
- } from "./chunk-IEW5RTJV.js";
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-SKT3O7KD.js");
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
- if (key === "api_key") {
326
- profile.apiKey = toStringValue(rawValue);
327
- continue;
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
- if (key === "apiKey") {
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: baseUrlOverride ?? profile.baseUrl ?? DEFAULT_PRODUCTION_BASE_URL,
570
- apiKey: apiKeyOverride ?? profile.apiKey,
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
- apiKey: typeof profile.apiKey === "string" ? profile.apiKey : ""
987
+ credential: describeProfileCredential(profile)
750
988
  })),
751
- ["name", "default", "baseUrl", "apiKey"],
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
- apiKey: profile.apiKey ?? ""
4595
+ credential: describeProfileCredential(profile)
4355
4596
  })),
4356
- ["name", "default", "baseUrl", "apiKey"],
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);