@mediamonks/canopy-cli 0.7.18 → 0.7.20
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/{db-BAaqh_2O.mjs → db-TaSLpQZ5.mjs} +2 -2
- package/dist/{deploy-DID3xhMJ.mjs → deploy-DCggsLD7.mjs} +2 -2
- package/dist/dev-DjkK-50R.mjs +2 -0
- package/dist/{dev-CjaomJ2D.mjs → dev-DtEwiNYi.mjs} +4 -4
- package/dist/docs/index.json +7 -2
- package/dist/docs/notifications.md +14 -1
- package/dist/docs/sdk-email.md +114 -0
- package/dist/docs/sdk.md +4 -4
- package/dist/{docs-DP_p_mmy.mjs → docs-C2D7__Iz.mjs} +2 -2
- package/dist/{egress-DDM-_7Yo.mjs → egress-BhbQfjjV.mjs} +1 -1
- package/dist/{feedback-DX99gDFp.mjs → feedback-BvkCjS-g.mjs} +2 -2
- package/dist/index.mjs +25 -25
- package/dist/{local-dAQdVFEK.mjs → local-CKRZur_5.mjs} +2 -2
- package/dist/login-D-Ac_kFW.mjs +285 -0
- package/dist/{logs-D60VRbHZ.mjs → logs-Beqz--Df.mjs} +1 -1
- package/dist/{migrations-kKKDlR_H.mjs → migrations-Bg9FEGFh.mjs} +1 -1
- package/dist/{open-B5XHmEmv.mjs → open-D7f1wNn3.mjs} +1 -1
- package/dist/{package-x-_qF58q.mjs → package-B-aXVcQu.mjs} +1 -1
- package/dist/{projects-TE1PVwMy.mjs → projects-1r5387ui.mjs} +1 -1
- package/dist/{schedule-Blldchmj.mjs → schedule-DK_Cv6Xf.mjs} +1 -1
- package/dist/{skill-CSFhZmBk.mjs → skill-CqiSkac0.mjs} +2 -2
- package/dist/{staging-Cw-Rzbsj.mjs → staging-CdpV2YHB.mjs} +1 -1
- package/dist/{token-BqxRIRkT.mjs → token-C8Bmzwfl.mjs} +1 -1
- package/dist/{ui-DLZNEen6.mjs → ui--atbZ-TM.mjs} +4 -0
- package/dist/{upgrade-DcU4YMr_.mjs → upgrade-CpUCVTrN.mjs} +2 -2
- package/dist/{usage-2cGnpSVc.mjs → usage-CRpVLLi6.mjs} +1 -1
- package/package.json +4 -3
- package/dist/dev-A2doKMCS.mjs +0 -2
- package/dist/login-CU0pev0k.mjs +0 -401
package/dist/login-CU0pev0k.mjs
DELETED
|
@@ -1,401 +0,0 @@
|
|
|
1
|
-
import { _ as writeConfigFile, f as clearAllProfileTokens, g as upsertProfileAuth, h as setActiveProfile, m as readConfigFile, n as siteConfig, p as listBakedProfiles, v as getBakedProfile, y as isProfileId } from "./site-BFaWfSVu.mjs";
|
|
2
|
-
import { n as openBrowser, r as reportErrors } from "./ui-DLZNEen6.mjs";
|
|
3
|
-
import { defineCommand } from "citty";
|
|
4
|
-
import * as p from "@clack/prompts";
|
|
5
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
6
|
-
import { createServer } from "node:http";
|
|
7
|
-
//#region src/login-page.ts
|
|
8
|
-
/**
|
|
9
|
-
* Branded HTML for the local OneLogin PKCE callback (127.0.0.1:8976).
|
|
10
|
-
* Visual language matches apps/gateway/src/auth-page.ts.
|
|
11
|
-
*/
|
|
12
|
-
function loginCallbackPage(ok) {
|
|
13
|
-
const title = ok ? "You're signed in" : "Sign-in didn’t finish";
|
|
14
|
-
return `<!doctype html>
|
|
15
|
-
<html lang="en">
|
|
16
|
-
<head>
|
|
17
|
-
<meta charset="utf-8">
|
|
18
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
19
|
-
<title>${title} · Canopy</title>
|
|
20
|
-
<style>
|
|
21
|
-
:root {
|
|
22
|
-
color-scheme: light dark;
|
|
23
|
-
--ink: #14201a;
|
|
24
|
-
--muted: #4a5c52;
|
|
25
|
-
--line: #d5ddd7;
|
|
26
|
-
--panel: rgba(255, 255, 255, 0.82);
|
|
27
|
-
--field: rgba(255, 255, 255, 0.7);
|
|
28
|
-
--accent: #2f6b4f;
|
|
29
|
-
--mark-ink: #f4faf6;
|
|
30
|
-
--shadow: 0 18px 40px -28px rgba(20, 32, 26, 0.45);
|
|
31
|
-
--bg: radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%),
|
|
32
|
-
radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%),
|
|
33
|
-
linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef);
|
|
34
|
-
--mark-bg: ${ok ? "var(--accent)" : "#8a4b3a"};
|
|
35
|
-
}
|
|
36
|
-
@media (prefers-color-scheme: dark) {
|
|
37
|
-
:root {
|
|
38
|
-
--ink: #e9f0ec;
|
|
39
|
-
--muted: #a2b3aa;
|
|
40
|
-
--line: rgba(233, 240, 236, 0.14);
|
|
41
|
-
--panel: rgba(24, 31, 27, 0.72);
|
|
42
|
-
--field: rgba(233, 240, 236, 0.06);
|
|
43
|
-
--accent: #7cc39d;
|
|
44
|
-
--mark-ink: #0d1410;
|
|
45
|
-
--shadow: 0 18px 44px -26px rgba(0, 0, 0, 0.7);
|
|
46
|
-
--bg: radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%),
|
|
47
|
-
radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%),
|
|
48
|
-
linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f);
|
|
49
|
-
--mark-bg: ${ok ? "var(--accent)" : "#e0a292"};
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
* { box-sizing: border-box; }
|
|
53
|
-
body {
|
|
54
|
-
margin: 0;
|
|
55
|
-
min-height: 100vh;
|
|
56
|
-
font-family: "Segoe UI", "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
|
|
57
|
-
color: var(--ink);
|
|
58
|
-
background: var(--bg);
|
|
59
|
-
display: grid;
|
|
60
|
-
place-items: center;
|
|
61
|
-
padding: 1.5rem;
|
|
62
|
-
}
|
|
63
|
-
main {
|
|
64
|
-
width: min(100%, 26rem);
|
|
65
|
-
background: var(--panel);
|
|
66
|
-
border: 1px solid var(--line);
|
|
67
|
-
border-radius: 1.25rem;
|
|
68
|
-
padding: 2rem 1.75rem 1.75rem;
|
|
69
|
-
backdrop-filter: blur(10px);
|
|
70
|
-
box-shadow: var(--shadow);
|
|
71
|
-
}
|
|
72
|
-
.mark {
|
|
73
|
-
width: 2.75rem;
|
|
74
|
-
height: 2.75rem;
|
|
75
|
-
border-radius: 0.85rem;
|
|
76
|
-
display: grid;
|
|
77
|
-
place-items: center;
|
|
78
|
-
background: var(--mark-bg);
|
|
79
|
-
color: var(--mark-ink);
|
|
80
|
-
margin-bottom: 1.25rem;
|
|
81
|
-
}
|
|
82
|
-
.mark svg { width: 1.4rem; height: 1.4rem; display: block; }
|
|
83
|
-
.brand {
|
|
84
|
-
margin: 0 0 0.35rem;
|
|
85
|
-
font-size: 0.8rem;
|
|
86
|
-
font-weight: 600;
|
|
87
|
-
letter-spacing: 0.04em;
|
|
88
|
-
text-transform: uppercase;
|
|
89
|
-
color: var(--accent);
|
|
90
|
-
}
|
|
91
|
-
h1 {
|
|
92
|
-
margin: 0 0 0.75rem;
|
|
93
|
-
font-size: 1.45rem;
|
|
94
|
-
line-height: 1.25;
|
|
95
|
-
letter-spacing: -0.02em;
|
|
96
|
-
font-weight: 650;
|
|
97
|
-
}
|
|
98
|
-
p {
|
|
99
|
-
margin: 0;
|
|
100
|
-
color: var(--muted);
|
|
101
|
-
font-size: 0.975rem;
|
|
102
|
-
line-height: 1.55;
|
|
103
|
-
}
|
|
104
|
-
.hint {
|
|
105
|
-
margin-top: 1.35rem;
|
|
106
|
-
padding-top: 1rem;
|
|
107
|
-
border-top: 1px solid var(--line);
|
|
108
|
-
font-size: 0.8rem;
|
|
109
|
-
color: var(--muted);
|
|
110
|
-
}
|
|
111
|
-
kbd {
|
|
112
|
-
font: inherit;
|
|
113
|
-
font-size: 0.85em;
|
|
114
|
-
padding: 0.1em 0.35em;
|
|
115
|
-
border: 1px solid var(--line);
|
|
116
|
-
border-radius: 0.35rem;
|
|
117
|
-
background: var(--field);
|
|
118
|
-
}
|
|
119
|
-
</style>
|
|
120
|
-
</head>
|
|
121
|
-
<body>
|
|
122
|
-
<main>
|
|
123
|
-
<div class="mark">${ok ? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
124
|
-
<path d="M20 6 9 17l-5-5"/>
|
|
125
|
-
</svg>` : `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
126
|
-
<circle cx="12" cy="12" r="9"/>
|
|
127
|
-
<path d="M12 8v5"/>
|
|
128
|
-
<path d="M12 16h.01"/>
|
|
129
|
-
</svg>`}</div>
|
|
130
|
-
<p class="brand">Canopy CLI</p>
|
|
131
|
-
<h1>${title}</h1>
|
|
132
|
-
<p>${ok ? "You can close this tab and return to the terminal — the CLI has your code." : "Return to the terminal and run canopy login again. Reloading this page won’t help."}</p>
|
|
133
|
-
<p class="hint">${ok ? "This tab only finishes the browser step — keep using the terminal for the rest." : "If the terminal still says it’s waiting, cancel with <kbd>Ctrl</kbd>+<kbd>C</kbd> and try again."}</p>
|
|
134
|
-
</main>
|
|
135
|
-
</body>
|
|
136
|
-
</html>`;
|
|
137
|
-
}
|
|
138
|
-
//#endregion
|
|
139
|
-
//#region src/login.ts
|
|
140
|
-
/**
|
|
141
|
-
* canopy login | whoami | logout — browser PKCE flow against OneLogin (public
|
|
142
|
-
* client), then trade the ID token for platform CLI tokens via POST
|
|
143
|
-
* /control/v1/auth/cli-token on every baked site (docs/10-cli-sites.md D17).
|
|
144
|
-
*/
|
|
145
|
-
const CALLBACK_PORT = 8976;
|
|
146
|
-
const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
|
|
147
|
-
const LOGIN_TIMEOUT_MS = 300 * 1e3;
|
|
148
|
-
const base64url = (buf) => buf.toString("base64url");
|
|
149
|
-
const whoamiCommand = defineCommand({
|
|
150
|
-
meta: {
|
|
151
|
-
name: "whoami",
|
|
152
|
-
description: "Show the current Canopy login"
|
|
153
|
-
},
|
|
154
|
-
run: reportErrors("whoami", async () => {
|
|
155
|
-
const config = siteConfig();
|
|
156
|
-
if (!config.token) throw new Error("Not logged in — run `canopy login`.");
|
|
157
|
-
console.log(config.email ?? "<unknown email>");
|
|
158
|
-
if (config.cloud) console.log(`Cloud: ${config.cloud}`);
|
|
159
|
-
console.log(`API: ${config.apiUrl}`);
|
|
160
|
-
if (config.tokenExpiresAt !== void 0) {
|
|
161
|
-
const expired = Date.parse(config.tokenExpiresAt) < Date.now();
|
|
162
|
-
console.log(expired ? `Token EXPIRED ${config.tokenExpiresAt} — run \`canopy login\` again.` : `Token expires ${config.tokenExpiresAt}.`);
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
});
|
|
166
|
-
const logoutCommand = defineCommand({
|
|
167
|
-
meta: {
|
|
168
|
-
name: "logout",
|
|
169
|
-
description: "Sign out: forget all local CLI tokens"
|
|
170
|
-
},
|
|
171
|
-
run: reportErrors("logout", async () => {
|
|
172
|
-
const file = readConfigFile();
|
|
173
|
-
if (!Object.values(file.profiles ?? {}).some((p) => p?.token)) {
|
|
174
|
-
console.log("Not logged in.");
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
clearAllProfileTokens();
|
|
178
|
-
console.log("Logged out — all site tokens removed from ~/.canopy/config.json.");
|
|
179
|
-
})
|
|
180
|
-
});
|
|
181
|
-
/** Wait for OneLogin to redirect back to the local loopback server. */
|
|
182
|
-
function waitForCallback(expectedState) {
|
|
183
|
-
return new Promise((resolve, reject) => {
|
|
184
|
-
const server = createServer((req, res) => {
|
|
185
|
-
const url = new URL(req.url ?? "/", REDIRECT_URI);
|
|
186
|
-
if (url.pathname !== "/callback") {
|
|
187
|
-
res.writeHead(404).end();
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const code = url.searchParams.get("code");
|
|
191
|
-
const state = url.searchParams.get("state");
|
|
192
|
-
const ok = Boolean(code) && state === expectedState;
|
|
193
|
-
res.writeHead(ok ? 200 : 400, {
|
|
194
|
-
"content-type": "text/html; charset=utf-8",
|
|
195
|
-
"cache-control": "no-store"
|
|
196
|
-
}).end(loginCallbackPage(ok));
|
|
197
|
-
server.close();
|
|
198
|
-
clearTimeout(timer);
|
|
199
|
-
if (ok) resolve(code);
|
|
200
|
-
else reject(new Error(url.searchParams.get("error_description") ?? "Login rejected"));
|
|
201
|
-
});
|
|
202
|
-
const timer = setTimeout(() => {
|
|
203
|
-
server.close();
|
|
204
|
-
reject(/* @__PURE__ */ new Error("Timed out waiting for the browser login (5 minutes)."));
|
|
205
|
-
}, LOGIN_TIMEOUT_MS);
|
|
206
|
-
server.on("error", (err) => reject(err.message.includes("EADDRINUSE") ? /* @__PURE__ */ new Error(`Port ${CALLBACK_PORT} is in use — close the other process and retry.`) : err));
|
|
207
|
-
server.listen(CALLBACK_PORT, "127.0.0.1");
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
async function pickActiveProfile(json, flag) {
|
|
211
|
-
if (flag) {
|
|
212
|
-
if (!isProfileId(flag)) throw new Error(`Unknown profile "${flag}". Choose one of: ${listBakedProfiles().map((p) => p.id).join(", ")}.`);
|
|
213
|
-
return flag;
|
|
214
|
-
}
|
|
215
|
-
const env = process.env["CANOPY_PROFILE"]?.trim();
|
|
216
|
-
if (env) {
|
|
217
|
-
if (!isProfileId(env)) throw new Error(`Unknown CANOPY_PROFILE "${env}".`);
|
|
218
|
-
return env;
|
|
219
|
-
}
|
|
220
|
-
const profiles = listBakedProfiles();
|
|
221
|
-
if (!process.stdin.isTTY || json) throw new Error(`Pass --profile <id> (or CANOPY_PROFILE) when not on a TTY. Options: ${profiles.map((p) => p.id).join(", ")}.`);
|
|
222
|
-
const choice = await p.select({
|
|
223
|
-
message: "Which Canopy site should be your default?",
|
|
224
|
-
options: profiles.map((prof) => ({
|
|
225
|
-
value: prof.id,
|
|
226
|
-
label: prof.label,
|
|
227
|
-
hint: prof.cloud
|
|
228
|
-
})),
|
|
229
|
-
initialValue: "main"
|
|
230
|
-
});
|
|
231
|
-
if (p.isCancel(choice)) {
|
|
232
|
-
p.cancel("Login cancelled.");
|
|
233
|
-
process.exit(1);
|
|
234
|
-
}
|
|
235
|
-
p.note(`Using profile "${choice}". Agents/non-TTY: canopy login --profile ${choice}`, "Active profile");
|
|
236
|
-
return choice;
|
|
237
|
-
}
|
|
238
|
-
async function mintForProfile(id, idToken) {
|
|
239
|
-
const baked = getBakedProfile(id);
|
|
240
|
-
try {
|
|
241
|
-
const cliTokenRes = await fetch(`${baked.apiUrl}/control/v1/auth/cli-token`, {
|
|
242
|
-
method: "POST",
|
|
243
|
-
headers: { "content-type": "application/json" },
|
|
244
|
-
body: JSON.stringify({ id_token: idToken })
|
|
245
|
-
});
|
|
246
|
-
if (!cliTokenRes.ok) return {
|
|
247
|
-
ok: false,
|
|
248
|
-
error: `${cliTokenRes.status} ${await cliTokenRes.text()}`
|
|
249
|
-
};
|
|
250
|
-
const { token, expires_at, user } = await cliTokenRes.json();
|
|
251
|
-
upsertProfileAuth(id, {
|
|
252
|
-
token,
|
|
253
|
-
tokenExpiresAt: expires_at,
|
|
254
|
-
email: user.email
|
|
255
|
-
});
|
|
256
|
-
return {
|
|
257
|
-
ok: true,
|
|
258
|
-
email: user.email,
|
|
259
|
-
expiresAt: expires_at
|
|
260
|
-
};
|
|
261
|
-
} catch (err) {
|
|
262
|
-
return {
|
|
263
|
-
ok: false,
|
|
264
|
-
error: err instanceof Error ? err.message : String(err)
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
async function login(json, profileFlag) {
|
|
269
|
-
if (!json) p.intro("canopy login");
|
|
270
|
-
const active = await pickActiveProfile(json, profileFlag);
|
|
271
|
-
const file = readConfigFile();
|
|
272
|
-
const { issuer, cliClientId } = {
|
|
273
|
-
issuer: process.env["CANOPY_OIDC_ISSUER"] ?? file.issuer ?? "https://s4capital.onelogin.com/oidc/2",
|
|
274
|
-
cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? file.cliClientId ?? "b69755d0-6678-013f-b5ce-09fe44a57aa0180501"
|
|
275
|
-
};
|
|
276
|
-
if (cliClientId.includes("REPLACE")) throw new Error("The CLI's OneLogin app is not configured yet (set CANOPY_OIDC_CLIENT_ID or packages/cli/src/config.ts).");
|
|
277
|
-
writeConfigFile({
|
|
278
|
-
...file,
|
|
279
|
-
issuer,
|
|
280
|
-
cliClientId,
|
|
281
|
-
activeProfile: active
|
|
282
|
-
});
|
|
283
|
-
const discoveryRes = await fetch(`${issuer}/.well-known/openid-configuration`);
|
|
284
|
-
if (!discoveryRes.ok) throw new Error(`OIDC discovery failed: ${discoveryRes.status}`);
|
|
285
|
-
const discovery = await discoveryRes.json();
|
|
286
|
-
const state = base64url(randomBytes(32));
|
|
287
|
-
const codeVerifier = base64url(randomBytes(32));
|
|
288
|
-
const authorizeUrl = new URL(discovery.authorization_endpoint);
|
|
289
|
-
authorizeUrl.search = new URLSearchParams({
|
|
290
|
-
response_type: "code",
|
|
291
|
-
client_id: cliClientId,
|
|
292
|
-
redirect_uri: REDIRECT_URI,
|
|
293
|
-
scope: "openid profile email",
|
|
294
|
-
state,
|
|
295
|
-
code_challenge: base64url(createHash("sha256").update(codeVerifier).digest()),
|
|
296
|
-
code_challenge_method: "S256"
|
|
297
|
-
}).toString();
|
|
298
|
-
const authorizeHref = authorizeUrl.toString();
|
|
299
|
-
openBrowser(authorizeHref);
|
|
300
|
-
const spin = json ? void 0 : p.spinner();
|
|
301
|
-
if (json) console.log(JSON.stringify({
|
|
302
|
-
status: "waiting",
|
|
303
|
-
url: authorizeHref
|
|
304
|
-
}));
|
|
305
|
-
else {
|
|
306
|
-
p.log.step("Opening OneLogin in your browser");
|
|
307
|
-
p.log.message("If nothing opens, visit:");
|
|
308
|
-
console.log(authorizeHref);
|
|
309
|
-
spin?.start("Waiting for you to sign in in the browser…");
|
|
310
|
-
}
|
|
311
|
-
let code;
|
|
312
|
-
try {
|
|
313
|
-
code = await waitForCallback(state);
|
|
314
|
-
} catch (err) {
|
|
315
|
-
spin?.error("Sign-in failed.");
|
|
316
|
-
throw err;
|
|
317
|
-
}
|
|
318
|
-
spin?.stop("Signed in with OneLogin.");
|
|
319
|
-
const tokenRes = await fetch(discovery.token_endpoint, {
|
|
320
|
-
method: "POST",
|
|
321
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
322
|
-
body: new URLSearchParams({
|
|
323
|
-
grant_type: "authorization_code",
|
|
324
|
-
client_id: cliClientId,
|
|
325
|
-
code,
|
|
326
|
-
redirect_uri: REDIRECT_URI,
|
|
327
|
-
code_verifier: codeVerifier
|
|
328
|
-
})
|
|
329
|
-
});
|
|
330
|
-
if (!tokenRes.ok) throw new Error(`Token exchange failed: ${tokenRes.status} ${await tokenRes.text()}`);
|
|
331
|
-
const { id_token: idToken } = await tokenRes.json();
|
|
332
|
-
if (!idToken) throw new Error("OneLogin returned no id_token.");
|
|
333
|
-
const mintSpin = json ? void 0 : p.spinner();
|
|
334
|
-
mintSpin?.start("Minting CLI tokens for each site…");
|
|
335
|
-
const results = [];
|
|
336
|
-
for (const prof of listBakedProfiles()) {
|
|
337
|
-
const result = await mintForProfile(prof.id, idToken);
|
|
338
|
-
results.push({
|
|
339
|
-
id: prof.id,
|
|
340
|
-
ok: result.ok,
|
|
341
|
-
detail: result.ok ? `expires ${result.expiresAt}` : result.error
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
mintSpin?.stop("Token mint finished.");
|
|
345
|
-
if (!results.find((r) => r.id === active)?.ok) {
|
|
346
|
-
const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : "✗"}`).join(" ");
|
|
347
|
-
throw new Error(`Could not mint a token for active profile "${active}". ${summary}. Re-run \`canopy login\`.`);
|
|
348
|
-
}
|
|
349
|
-
setActiveProfile(active);
|
|
350
|
-
const email = readConfigFile().profiles?.[active]?.email;
|
|
351
|
-
const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : `✗ (${r.detail})`}`).join("\n");
|
|
352
|
-
if (json) console.log(JSON.stringify({
|
|
353
|
-
status: "ok",
|
|
354
|
-
email,
|
|
355
|
-
activeProfile: active,
|
|
356
|
-
profiles: Object.fromEntries(results.map((r) => [r.id, {
|
|
357
|
-
ok: r.ok,
|
|
358
|
-
detail: r.detail
|
|
359
|
-
}]))
|
|
360
|
-
}));
|
|
361
|
-
else {
|
|
362
|
-
p.note(summary, "Sites");
|
|
363
|
-
p.outro(`Logged in as ${email ?? "unknown"} — default profile "${active}".`);
|
|
364
|
-
for (const r of results.filter((x) => !x.ok)) p.log.warn(`${r.id} failed — re-run \`canopy login\` to retry all sites.`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
const loginCommand = defineCommand({
|
|
368
|
-
meta: {
|
|
369
|
-
name: "login",
|
|
370
|
-
description: "Sign in to Canopy via OneLogin"
|
|
371
|
-
},
|
|
372
|
-
args: {
|
|
373
|
-
json: {
|
|
374
|
-
type: "boolean",
|
|
375
|
-
default: false,
|
|
376
|
-
description: "Emit machine-readable status to stdout (for agents/CI)"
|
|
377
|
-
},
|
|
378
|
-
profile: {
|
|
379
|
-
type: "string",
|
|
380
|
-
description: "Active default profile (main | staging | gcp-pilot); required non-TTY"
|
|
381
|
-
}
|
|
382
|
-
},
|
|
383
|
-
run: reportErrors("login", async ({ args }) => {
|
|
384
|
-
const json = args.json;
|
|
385
|
-
try {
|
|
386
|
-
await login(json, args.profile);
|
|
387
|
-
} catch (err) {
|
|
388
|
-
if (json) {
|
|
389
|
-
console.log(JSON.stringify({
|
|
390
|
-
status: "error",
|
|
391
|
-
message: err instanceof Error ? err.message : String(err)
|
|
392
|
-
}));
|
|
393
|
-
process.exitCode = 1;
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
throw err;
|
|
397
|
-
}
|
|
398
|
-
})
|
|
399
|
-
});
|
|
400
|
-
//#endregion
|
|
401
|
-
export { loginCommand, logoutCommand, whoamiCommand };
|