@zivis/cli 0.1.0-alpha.39 → 0.1.0-alpha.40

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.
@@ -46,7 +46,7 @@ export function registerAppCommands(program) {
46
46
  .description("create a customer-facing Application configured for ephemeral PKCE auth")
47
47
  .requiredOption("--name <name>", "application display name")
48
48
  .requiredOption("--base-url <url>", "primary base URL (e.g., https://api.acme.io)")
49
- .option("--type <type>", "agent | mcp_server | llm_endpoint | rest_api | ad_hoc_url", "agent")
49
+ .option("--type <type>", "application | agent | mcp_server | llm_endpoint | rest_api | ad_hoc_url", "agent")
50
50
  .option("--description <text>", "optional description")
51
51
  .option("--client-id <id>", "OAuth client_id (PKCE public client)")
52
52
  .option("--oidc-issuer <url>", "OIDC issuer (for discovery)")
@@ -105,7 +105,7 @@ export function registerAppCommands(program) {
105
105
  .description("update an application's metadata (name, type, ephemeral-creds toggle, etc.)")
106
106
  .option("--name <name>", "rename the application")
107
107
  .option("--description <text>", "update the description")
108
- .option("--type <type>", "agent | mcp_server | llm_endpoint | rest_api | ad_hoc_url")
108
+ .option("--type <type>", "application | agent | mcp_server | llm_endpoint | rest_api | ad_hoc_url")
109
109
  .option("--use-ephemeral", "opt into ephemeral-creds mode (Phase J)")
110
110
  .option("--no-ephemeral-disable", "opt OUT of ephemeral-creds mode (legacy KV-backed)")
111
111
  .option("--base-url <url>", "update base URL")
@@ -28,6 +28,7 @@ export function registerAuthCommands(program) {
28
28
  .option("-s, --session <name>", "named session for multi-tenant")
29
29
  .option("-o, --org <workosOrgId>", "scope JWT to a specific WorkOS org")
30
30
  .option("-y, --yes", "skip interactive confirmation")
31
+ .option("--rebind", "switch this project's bound org via interactive picker")
31
32
  .option("--bind", "auto-write .zivis/project.json binding")
32
33
  .option("--no-bind", "skip project binding")
33
34
  .option("--skip-mint-key", "skip MCP API key mint after successful OAuth")
@@ -4,6 +4,7 @@ export interface LoginCmdOpts {
4
4
  yes?: boolean;
5
5
  bind?: boolean;
6
6
  noBind?: boolean;
7
+ rebind?: boolean;
7
8
  skipMintKey?: boolean;
8
9
  }
9
10
  export declare function loginCommand(opts: LoginCmdOpts): Promise<void>;
@@ -1,13 +1,25 @@
1
+ import * as readline from "node:readline/promises";
1
2
  import { login } from "@zivis/mcp/auth";
2
3
  import { getStorageName } from "@zivis/mcp/auth";
3
4
  import { saveSessionEntry } from "@zivis/mcp/auth";
4
5
  import { detectProjectBinding, resolveConfigFromBinding } from "@zivis/mcp/project-binding";
5
6
  import { credentialStorageFromConfig, DEFAULT_CONFIG } from "@zivis/mcp/types";
6
7
  import { extractEmail } from "../../internal/orgs.js";
8
+ import { pickOrg } from "../../internal/org-picker.js";
7
9
  import { ensureProjectBinding } from "../../internal/project-binding.js";
8
10
  import { discardOauthAfterLogin, tryAutoMintBindingTokenAfterLogin, } from "../../internal/binding-token-mint.js";
9
11
  import { setupCursorWorkspace, setupVsCodeClaude, setupVsCodeCopilot, setupClaudeCodeCli, } from "../../internal/ide-setup.js";
10
12
  import { buildLinkagePrompt } from "../../internal/github-linkage-prompt.js";
13
+ async function confirm(question) {
14
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
15
+ try {
16
+ const answer = (await rl.question(question)).trim().toLowerCase();
17
+ return answer !== "n" && answer !== "no";
18
+ }
19
+ finally {
20
+ rl.close();
21
+ }
22
+ }
11
23
  export async function loginCommand(opts) {
12
24
  const config = {
13
25
  ...DEFAULT_CONFIG,
@@ -15,29 +27,82 @@ export async function loginCommand(opts) {
15
27
  };
16
28
  const detected = detectProjectBinding(process.cwd());
17
29
  const session = opts.session || detected?.binding.session || config.session;
18
- let organizationId = opts.org;
19
- if (!organizationId && detected?.binding.workosOrgId) {
30
+ const sessionName = session || "default";
31
+ let organizationId;
32
+ let orgName;
33
+ let writeMode = "no-write";
34
+ if (opts.org) {
35
+ organizationId = opts.org;
36
+ orgName = detected?.binding.orgName && detected.binding.workosOrgId === opts.org
37
+ ? detected.binding.orgName
38
+ : undefined;
39
+ if (opts.rebind) {
40
+ writeMode = "replace";
41
+ }
42
+ }
43
+ else if (opts.rebind) {
44
+ const picked = await runIdentityThenPicker(config, sessionName, {
45
+ currentOrgId: detected?.binding.workosOrgId ?? undefined,
46
+ header: detected?.binding.workosOrgId
47
+ ? "Switch this project to which org?"
48
+ : "Your organizations:",
49
+ });
50
+ if (detected?.binding.workosOrgId && detected.binding.workosOrgId === picked.workosOrgId) {
51
+ console.log(`Project is already bound to ${picked.name}. Nothing to change.`);
52
+ process.exit(0);
53
+ }
54
+ if (detected?.binding.workosOrgId) {
55
+ const fromLabel = detected.binding.orgName
56
+ ? `${detected.binding.orgName} (${detected.binding.workosOrgId})`
57
+ : detected.binding.workosOrgId;
58
+ const toLabel = `${picked.name} (${picked.workosOrgId})`;
59
+ if (!opts.yes) {
60
+ const ok = await confirm(`Change project binding from "${fromLabel}" to "${toLabel}"? [Y/n] `);
61
+ if (!ok) {
62
+ console.log("Aborted.");
63
+ process.exit(0);
64
+ }
65
+ }
66
+ writeMode = "replace";
67
+ }
68
+ else {
69
+ writeMode = "create";
70
+ }
71
+ organizationId = picked.workosOrgId;
72
+ orgName = picked.name;
73
+ }
74
+ else if (detected?.binding.workosOrgId) {
20
75
  organizationId = detected.binding.workosOrgId;
21
- console.log(`Using org from project binding: ${organizationId}${detected.binding.orgName ? ` (${detected.binding.orgName})` : ""}`);
76
+ orgName = detected.binding.orgName;
77
+ console.log(`Using org from project binding: ${organizationId}${orgName ? ` (${orgName})` : ""}`);
78
+ }
79
+ else {
80
+ const picked = await runIdentityThenPicker(config, sessionName, {
81
+ header: "Your organizations:",
82
+ });
83
+ organizationId = picked.workosOrgId;
84
+ orgName = picked.name;
85
+ writeMode = "create";
86
+ }
87
+ if (!organizationId) {
88
+ console.error("Internal error: no organization resolved for login.");
89
+ process.exit(1);
22
90
  }
23
91
  if (detected?.binding.session && !opts.session) {
24
92
  console.log(`Using session from project binding: ${session}`);
25
93
  }
26
94
  try {
27
95
  const tokens = await login(config, session, organizationId, {
28
- orgName: detected?.binding.orgName,
29
- skipConfirm: opts.yes === true,
96
+ orgName,
97
+ skipConfirm: opts.yes === true || writeMode !== "no-write",
30
98
  });
31
99
  const storageName = getStorageName();
32
100
  const email = extractEmail(tokens.id_token);
33
- const sessionName = session || "default";
34
101
  await saveSessionEntry(sessionName, email, credentialStorageFromConfig(config));
35
102
  const sessionLabel = session ? ` (session: ${session})` : "";
36
103
  console.log(`✓ Logged in as ${email}${sessionLabel}`);
37
104
  console.log(`✓ Credentials stored in ${storageName}`);
38
- if (organizationId) {
39
- console.log(`✓ JWT scoped to organization: ${organizationId}`);
40
- }
105
+ console.log(`✓ JWT scoped to organization: ${organizationId}${orgName ? ` (${orgName})` : ""}`);
41
106
  try {
42
107
  await setupCursorWorkspace();
43
108
  }
@@ -57,14 +122,24 @@ export async function loginCommand(opts) {
57
122
  catch (err) {
58
123
  console.log(` WARNING: Failed to update Claude Code CLI config: ${err instanceof Error ? err.message : err}`);
59
124
  }
125
+ if (writeMode === "create" && !opts.yes && !opts.noBind) {
126
+ const orgLabel = orgName ? `${orgName} (${organizationId})` : organizationId;
127
+ const ok = await confirm(`Bind this project to ${orgLabel}? [Y/n] `);
128
+ if (!ok) {
129
+ console.log("Login succeeded but project was not bound.");
130
+ await discardOauthAfterLogin({ session, config });
131
+ process.exit(0);
132
+ }
133
+ }
60
134
  await ensureProjectBinding({
61
135
  config,
62
136
  session: sessionName,
63
137
  accessToken: tokens.access_token,
64
138
  organizationId,
65
139
  cwd: process.cwd(),
66
- autoBind: opts.bind === true,
140
+ autoBind: opts.bind === true || writeMode !== "no-write",
67
141
  skipBind: opts.noBind === true,
142
+ force: writeMode === "replace",
68
143
  });
69
144
  await tryAutoMintBindingTokenAfterLogin({
70
145
  cwd: process.cwd(),
@@ -88,3 +163,21 @@ export async function loginCommand(opts) {
88
163
  process.exit(1);
89
164
  }
90
165
  }
166
+ async function runIdentityThenPicker(config, session, pickOpts) {
167
+ console.log("Opening browser to identify your account (no org scope yet)...");
168
+ let identity;
169
+ try {
170
+ identity = await login(config, session, undefined, { skipConfirm: true });
171
+ }
172
+ catch (err) {
173
+ console.error("Login failed:", err instanceof Error ? err.message : err);
174
+ process.exit(1);
175
+ }
176
+ try {
177
+ return await pickOrg(config, identity.access_token, pickOpts);
178
+ }
179
+ catch (err) {
180
+ console.error(err instanceof Error ? err.message : err);
181
+ process.exit(1);
182
+ }
183
+ }
@@ -126,6 +126,7 @@ function appTypeToTargetType(appType) {
126
126
  return "ai_chat";
127
127
  case "rest_api":
128
128
  return "api";
129
+ case "application":
129
130
  case "ad_hoc_url":
130
131
  return "web_app";
131
132
  default:
@@ -0,0 +1,11 @@
1
+ import type { ZivisConfig } from "@zivis/mcp/types";
2
+ import { type OrgInfo } from "./orgs.js";
3
+ export interface PickOrgOpts {
4
+ currentOrgId?: string;
5
+ header?: string;
6
+ promptText?: string;
7
+ }
8
+ export interface PickedOrg extends OrgInfo {
9
+ workosOrgId: string;
10
+ }
11
+ export declare function pickOrg(config: ZivisConfig, identityAccessToken: string, opts?: PickOrgOpts): Promise<PickedOrg>;
@@ -0,0 +1,34 @@
1
+ import * as readline from "node:readline/promises";
2
+ import { fetchUserOrgs } from "./orgs.js";
3
+ export async function pickOrg(config, identityAccessToken, opts = {}) {
4
+ const all = await fetchUserOrgs(config, identityAccessToken);
5
+ const eligible = all.filter((o) => typeof o.workosOrgId === "string" && o.workosOrgId.length > 0);
6
+ if (eligible.length === 0) {
7
+ throw new Error("No active organizations available for this account. " +
8
+ "Ask an org admin to invite you, or contact support.");
9
+ }
10
+ if (eligible.length === 1) {
11
+ const only = eligible[0];
12
+ console.log(`One organization available: ${only.name}`);
13
+ return only;
14
+ }
15
+ console.log("");
16
+ console.log(opts.header ?? "Your organizations:");
17
+ eligible.forEach((o, i) => {
18
+ const marker = opts.currentOrgId && o.workosOrgId === opts.currentOrgId ? " (current)" : "";
19
+ console.log(` ${i + 1}. ${o.name}${marker}`);
20
+ });
21
+ const promptText = opts.promptText ?? `\nSelect org [1-${eligible.length}]: `;
22
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
23
+ try {
24
+ const answer = (await rl.question(promptText)).trim();
25
+ const idx = Number.parseInt(answer, 10) - 1;
26
+ if (Number.isNaN(idx) || idx < 0 || idx >= eligible.length) {
27
+ throw new Error("Invalid selection.");
28
+ }
29
+ return eligible[idx];
30
+ }
31
+ finally {
32
+ rl.close();
33
+ }
34
+ }
@@ -54,12 +54,20 @@ function tokensFromResponse(d, prevRefresh) {
54
54
  obtained_at: Date.now(),
55
55
  };
56
56
  }
57
+ function escapeHtml(s) {
58
+ return s
59
+ .replace(/&/g, "&amp;")
60
+ .replace(/</g, "&lt;")
61
+ .replace(/>/g, "&gt;")
62
+ .replace(/"/g, "&quot;")
63
+ .replace(/'/g, "&#39;");
64
+ }
57
65
  function htmlPage(success, msg = "") {
58
66
  const title = success ? "Authenticated" : "Auth failed";
59
67
  const color = success ? "#00d4aa" : "#ff5252";
60
68
  return `<!DOCTYPE html><html><head><title>${title}</title></head>
61
69
  <body style="font-family:system-ui;text-align:center;padding:50px;background:#0f0f1a;color:#fff;">
62
- <h1 style="color:${color};">${title}</h1><p>${msg}</p>
70
+ <h1 style="color:${color};">${title}</h1><p>${escapeHtml(msg)}</p>
63
71
  <p>You can close this window.</p><script>setTimeout(()=>window.close(),1500);</script>
64
72
  </body></html>`;
65
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zivis/cli",
3
- "version": "0.1.0-alpha.39",
3
+ "version": "0.1.0-alpha.40",
4
4
  "description": "ZIVIS CLI — threat modeling, scans, and MCP server for IDE integration",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://zivis.ai",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "publishConfig": {
23
- "access": "restricted"
23
+ "access": "public"
24
24
  },
25
25
  "engines": {
26
26
  "node": ">=20.0.0"
@@ -32,7 +32,7 @@
32
32
  "ignore": "^7.0.5",
33
33
  "open": "^10.1.0",
34
34
  "web-tree-sitter": "^0.26.8",
35
- "@zivis/mcp": "0.1.0-alpha.26"
35
+ "@zivis/mcp": "0.1.5"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/js-yaml": "^4.0.9",