@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.
- package/dist/commands/app/index.js +2 -2
- package/dist/commands/auth/index.js +1 -0
- package/dist/commands/auth/login.d.ts +1 -0
- package/dist/commands/auth/login.js +103 -10
- package/dist/commands/scan/index.js +1 -0
- package/dist/internal/org-picker.d.ts +11 -0
- package/dist/internal/org-picker.js +34 -0
- package/dist/internal/target-auth/workos.js +9 -1
- package/package.json +3 -3
|
@@ -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")
|
|
@@ -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
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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, "&")
|
|
60
|
+
.replace(/</g, "<")
|
|
61
|
+
.replace(/>/g, ">")
|
|
62
|
+
.replace(/"/g, """)
|
|
63
|
+
.replace(/'/g, "'");
|
|
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.
|
|
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": "
|
|
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.
|
|
35
|
+
"@zivis/mcp": "0.1.5"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/js-yaml": "^4.0.9",
|