@mediamonks/canopy-cli 0.7.19 → 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.
@@ -80,7 +80,7 @@
80
80
  {
81
81
  "slug": "notifications",
82
82
  "title": "Slack notifications",
83
- "summary": "Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules."
83
+ "summary": "Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules — and answers questions when you ask."
84
84
  },
85
85
  {
86
86
  "slug": "static-serving",
@@ -296,7 +296,7 @@
296
296
  {
297
297
  "slug": "sdk",
298
298
  "title": "Using the SDK",
299
- "summary": "One typed client for auth, data, files, AI, and services — no keys, no connection strings, the platform authenticates for you."
299
+ "summary": "One typed client for auth, data, files, AI, email, and services — no keys, no connection strings, the platform authenticates for you."
300
300
  },
301
301
  {
302
302
  "slug": "sdk-auth",
@@ -318,6 +318,11 @@
318
318
  "title": "AI",
319
319
  "summary": "Metered LLM access through the project's budget-capped gateway key — chat, streaming, and an OpenAI-compatible endpoint."
320
320
  },
321
+ {
322
+ "slug": "sdk-email",
323
+ "title": "Email",
324
+ "summary": "Send email from your project without a mail server or an API key — Monks recipients only, with a per-project daily quota and a sent log."
325
+ },
321
326
  {
322
327
  "slug": "sdk-google",
323
328
  "title": "Google",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Slack notifications
3
- description: Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules.
3
+ description: Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules — and answers questions when you ask.
4
4
  ---
5
5
 
6
6
  Canopy sends you Slack DMs when something on the platform needs your
@@ -38,6 +38,19 @@ the categories on or off. To silence one noisy project entirely, use **Mute
38
38
  project** on its Settings tab — personal to you, and your own reports
39
39
  elsewhere keep notifying.
40
40
 
41
+ ## Asking Canopy things
42
+
43
+ The same bot answers questions. DM **Canopy** in Slack — or mention
44
+ **@Canopy** in a channel — and ask in plain language: "what projects do I
45
+ have?", "why is my fetch to that API failing?", "how do I roll back a bad
46
+ deploy?". It looks at your projects (only the ones you can already see) and
47
+ at these docs, and answers with links to the right guide.
48
+
49
+ It can only read. It never deploys, changes settings, or touches your data —
50
+ for that it points you at the dashboard or the CLI. In a channel that's
51
+ linked to a project, it already knows which project you mean; replying in a
52
+ report's notification thread, it knows which report.
53
+
41
54
  ## If DMs don't arrive
42
55
 
43
56
  The bot finds you by your work email. If your Slack account uses a different
@@ -0,0 +1,114 @@
1
+ ---
2
+ title: Email
3
+ description: Send email from your project without a mail server or an API key — Monks recipients only, with a per-project daily quota and a sent log.
4
+ ---
5
+
6
+ `canopy.email` sends transactional email — a digest, a notification, a
7
+ "your request was approved" — from your project code. There is no mail
8
+ server to run, no provider account to open, no API key in your project, and
9
+ no DNS to configure. The platform sends on your behalf and keeps a log of
10
+ what went out.
11
+
12
+ Two rules are enforced by the platform, not by your code:
13
+
14
+ - **Monks recipients only.** Every address must be an `@monks.com` one.
15
+ Anything else is refused before the message is sent.
16
+ - **A daily quota per project**, 100 emails by default. Each recipient
17
+ counts as one. Ask an admin if your project genuinely needs more.
18
+
19
+ ## email.send(options)
20
+
21
+ Resolves once the platform has accepted the message:
22
+
23
+ ```ts
24
+ await canopy.email.send({
25
+ to: "someone@monks.com",
26
+ subject: "Weekly digest",
27
+ text: "Three new entries this week.",
28
+ });
29
+ ```
30
+
31
+ **`EmailSendOptions`:**
32
+
33
+ - `to` — required. One address, or an array of up to 10.
34
+ - `subject` — required, up to 200 characters.
35
+ - `text` — required. The plain-text body. Always sent, even alongside
36
+ `html`: some clients show only this, and it helps you stay out of spam
37
+ folders.
38
+ - `html` — optional HTML body. `text` and `html` together are capped at
39
+ 256 KB.
40
+ - `replyTo` — optional. Follows the same recipient rule as `to`, so replies
41
+ cannot be routed off to an outside address.
42
+
43
+ **Returns** `{ delivered, queued, bounced }` — three arrays of addresses.
44
+ `delivered` is accepted for delivery, `queued` will be retried, and
45
+ `bounced` will never arrive (a wrong address, usually).
46
+
47
+ Attachments are not supported. Link to something in your app instead.
48
+
49
+ ## Who the email comes from
50
+
51
+ Your project sends as itself: the From address is your project's slug at the
52
+ platform's mail domain, with your project's name attached — so a recipient
53
+ can tell at a glance which app is writing to them, and replies do not land
54
+ in a shared inbox.
55
+
56
+ Set `replyTo` when you want answers to reach a real person or team.
57
+
58
+ ## Sending from the backend
59
+
60
+ The most useful sends have nobody signed in — a nightly digest, a
61
+ [scheduled](https://docs.canopy.website/platform/schedules) summary. Email works there too: call it from
62
+ a worker route or a scheduled handler and it authenticates as the project
63
+ itself.
64
+
65
+ ```ts
66
+ export default {
67
+ async scheduled(event, env) {
68
+ const canopy = createCanopy({ fetch: env.CANOPY.fetch.bind(env.CANOPY) });
69
+ const entries = await recentEntries(env.DB);
70
+
71
+ await canopy.email.send({
72
+ to: "team@monks.com",
73
+ subject: `Digest · ${entries.length} new entries`,
74
+ text: entries.map((e) => `· ${e.title}`).join("\n"),
75
+ });
76
+ },
77
+ };
78
+ ```
79
+
80
+ ## Seeing what was sent
81
+
82
+ Your project's **Usage** tab in the dashboard shows how much of today's
83
+ quota you have used and the recent sends — when, to whom, the subject, and
84
+ whether it went out. Message bodies are never stored.
85
+
86
+ ## When it fails
87
+
88
+ Every failure is a `CanopyError` with a status you can branch on:
89
+
90
+ - **400** — the recipient isn't a Monks address, or the request broke one of
91
+ the limits above (too many recipients, subject too long, missing `text`).
92
+ - **429** — the project used its daily quota. It resets at UTC midnight.
93
+ - **501** — email isn't set up on this Canopy site.
94
+ - **502** — the platform accepted the request but the send failed downstream.
95
+
96
+ ```ts
97
+ import { CanopyError } from "@mediamonks/canopy";
98
+
99
+ try {
100
+ await canopy.email.send({ to, subject, text });
101
+ } catch (err) {
102
+ if (err instanceof CanopyError && err.status === 429) {
103
+ // Out of quota for today — queue it, or tell the user.
104
+ }
105
+ }
106
+ ```
107
+
108
+ ## What this is not
109
+
110
+ This is transactional email for a small internal audience. It is not a
111
+ newsletter platform: there are no mailing lists, no subscriber management,
112
+ and no unsubscribe handling — if you send something people can opt into,
113
+ you own that list and that opt-out inside your own app. A project that
114
+ outgrows "email some Monks" should graduate to a dedicated email provider.
package/dist/docs/sdk.md CHANGED
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  title: Using the SDK
3
- description: One typed client for auth, data, files, AI, and services — no keys, no connection strings, the platform authenticates for you.
3
+ description: One typed client for auth, data, files, AI, email, and services — no keys, no connection strings, the platform authenticates for you.
4
4
  ---
5
5
 
6
6
  `@mediamonks/canopy` is a thin, typed client over the platform's project API.
7
- One `createCanopy()` call gives you six modules — `auth`, `db`, `blob`, `ai`,
8
- `google`, and `services` — and that is the entire surface. There are no API keys,
7
+ One `createCanopy()` call gives you seven modules — `auth`, `db`, `blob`, `ai`,
8
+ `email`, `google`, and `services` — and that is the entire surface. There are no API keys,
9
9
  connection strings, or environment variables to manage: every call rides the
10
10
  session the platform already established before your code ran.
11
11
 
@@ -35,7 +35,7 @@ import { createCanopy } from "/_api/data/v1/sdk.js";
35
35
  import { createCanopy } from "@mediamonks/canopy";
36
36
 
37
37
  const canopy = createCanopy();
38
- // canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.google, canopy.services
38
+ // canopy.auth, canopy.db, canopy.blob, canopy.ai, canopy.email, canopy.google, canopy.services
39
39
  ```
40
40
 
41
41
  | Option | Type | Default | What it does |
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-qe7a3VEV.mjs";
1
+ import { t as version } from "./package-B-aXVcQu.mjs";
2
2
  import { n as siteConfig } from "./site-BFaWfSVu.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { r as reportErrors } from "./ui--atbZ-TM.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-qe7a3VEV.mjs";
1
+ import { t as version } from "./package-B-aXVcQu.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BFaWfSVu.mjs";
3
3
  import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui--atbZ-TM.mjs";
4
4
  import { defineCommand } from "citty";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-qe7a3VEV.mjs";
2
+ import { t as version } from "./package-B-aXVcQu.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BFaWfSVu.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,9 +60,9 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-D9eO1s7W.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-D9eO1s7W.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-D9eO1s7W.mjs").then((m) => m.logoutCommand),
63
+ login: () => import("./login-D-Ac_kFW.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-D-Ac_kFW.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-D-Ac_kFW.mjs").then((m) => m.logoutCommand),
66
66
  create: () => import("./projects-1r5387ui.mjs").then((m) => m.createCommand),
67
67
  init: () => import("./projects-1r5387ui.mjs").then((m) => m.initCommand),
68
68
  projects: () => import("./projects-1r5387ui.mjs").then((m) => m.projectsCommand),
@@ -76,13 +76,13 @@ const main = defineCommand({
76
76
  token: () => import("./token-C8Bmzwfl.mjs").then((m) => m.tokenCommand),
77
77
  egress: () => import("./egress-BhbQfjjV.mjs").then((m) => m.egressCommand),
78
78
  schedule: () => import("./schedule-DK_Cv6Xf.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-DBgUpmDC.mjs").then((m) => m.feedbackCommand),
79
+ feedback: () => import("./feedback-BvkCjS-g.mjs").then((m) => m.feedbackCommand),
80
80
  db: () => import("./db-TaSLpQZ5.mjs").then((m) => m.dbCommand),
81
81
  usage: () => import("./usage-CRpVLLi6.mjs").then((m) => m.usageCommand),
82
82
  open: () => import("./open-D7f1wNn3.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-CKZV1X-B.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-DW68KWTY.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-DNOIy4g3.mjs").then((m) => m.upgradeCommand),
83
+ docs: () => import("./docs-C2D7__Iz.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-CqiSkac0.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-CpUCVTrN.mjs").then((m) => m.upgradeCommand),
86
86
  logs: () => import("./logs-Beqz--Df.mjs").then((m) => m.logsCommand)
87
87
  }
88
88
  });
@@ -0,0 +1,285 @@
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--atbZ-TM.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 ../pages/src/generated/cli-login.ts
8
+ const okHtml = "<!doctype html><html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>You’re signed in · Canopy</title><style>/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:\"Helvetica Now for Monks\", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;--color-red-100:#ffebf0;--color-red-200:#ffd1dd;--color-red-300:#ffa8be;--color-red-400:#ff7a9c;--color-red-500:#ff527d;--color-red-600:#ff245b;--color-red-800:#a30029;--color-red-950:#610018;--color-amber-200:#fde68a;--color-amber-950:#451a03;--color-yellow-100:#fff7db;--color-yellow-900:#7a6000;--color-green-100:#e3fcf1;--color-green-200:#b8fadd;--color-green-900:#067445;--color-green-950:#065533;--color-purple-50:#faf6fd;--color-purple-300:#d6b6f1;--color-purple-400:#c598ec;--color-purple-500:#ac6de3;--color-purple-600:#8c3fd2;--color-purple-700:#7d26c9;--color-purple-900:#501881;--color-gray-800:#5d5d60;--color-neutral-50:#f5f5f4;--color-neutral-100:#e9e8e7;--color-neutral-200:#d6d4d2;--color-neutral-300:#c0beb9;--color-neutral-400:#adaaa4;--color-neutral-500:#97938c;--color-neutral-600:#837f77;--color-neutral-700:#6c6962;--color-neutral-900:#484641;--color-neutral-950:#3e3c38;--color-stone-500:#78716c;--color-stone-600:#57534e;--color-stone-700:#44403c;--color-stone-800:#292524;--color-stone-900:#1c1917;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--text-xs:12px;--text-xs--line-height:16px;--text-sm:14px;--text-sm--line-height:20px;--text-2xl:24px;--text-2xl--line-height:32px;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.01em;--tracking-wide:.015em;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-md:6px;--radius-lg:8px;--radius-xl:12px;--radius-2xl:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-foreground:var(--foreground);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground);--color-muted-foreground:var(--muted-foreground);--color-destructive:var(--destructive);--color-border:var(--border);--color-card:var(--card);--color-brand:var(--brand);--color-brand-foreground:var(--brand-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root,[data-theme=monks-flow]{--background:var(--color-white);--background-emphasis:var(--color-neutral-900);--foreground:var(--color-neutral-950);--foreground-alt:var(--color-neutral-700);--foreground-subtle:var(--color-neutral-600);--primary:var(--color-neutral-900);--primary-foreground:var(--color-neutral-50);--primary-hover:var(--color-neutral-950);--secondary:#f5f3f0;--secondary-foreground:var(--color-neutral-900);--secondary-hover:#e7e2dc;--muted:var(--color-neutral-100);--muted-foreground:var(--color-neutral-500);--accent:#e7e2dc;--accent-foreground:var(--color-neutral-900);--destructive:#e11d48;--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-100);--destructive-soft-foreground:var(--color-red-800);--ghost:#fff0;--ghost-foreground:var(--color-neutral-700);--ghost-hover:#0000000d;--positive:var(--color-green-100);--positive-foreground:var(--color-green-900);--surface:var(--color-neutral-50);--surface-emphasis:var(--color-neutral-300);--surface-raised:var(--color-neutral-200);--border:var(--color-neutral-200);--border-light:var(--color-neutral-100);--border-lighter:var(--color-neutral-50);--border-strong:var(--color-neutral-400);--input:var(--color-neutral-50);--input-foreground:var(--color-neutral-950);--ring:var(--color-black);--ring-error:var(--color-red-300);--card:var(--color-white);--card-foreground:var(--color-neutral-950);--popover:var(--color-white);--popover-foreground:var(--color-neutral-950);--warning:var(--color-yellow-100);--warning-foreground:var(--color-yellow-900);--chart-1:#f54a00;--chart-2:#009689;--chart-3:#104e64;--chart-4:#ffb900;--chart-5:#fe9a00;--chart-negative:#f54a00;--chart-positive:#009689;--chart-fill:#bfdeffb3;--chart-fill-2:#aaccffb3;--chart-stroke-2:#3f8dff;--sidebar:var(--color-neutral-50);--sidebar-foreground:var(--color-gray-800);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-neutral-200);--sidebar-ring:var(--ring);--backdrop:#0009;--theme-background:#f5f4f2}:root.dark:not([data-theme]),[data-theme=monks-flow].dark,[data-theme=monks-flow] .dark{--background:var(--color-stone-900);--background-emphasis:var(--color-neutral-100);--foreground:var(--color-neutral-50);--foreground-alt:var(--color-neutral-300);--foreground-subtle:var(--color-neutral-400);--primary:var(--color-neutral-50);--primary-foreground:var(--color-black);--primary-hover:var(--color-neutral-100);--secondary:var(--color-stone-700);--secondary-foreground:var(--color-neutral-100);--secondary-hover:var(--color-stone-600);--muted:var(--color-stone-800);--muted-foreground:var(--color-neutral-500);--accent:var(--color-neutral-900);--accent-foreground:var(--color-neutral-300);--destructive:var(--color-red-500);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-950);--destructive-soft-foreground:var(--color-red-200);--ghost:#fff0;--ghost-foreground:var(--color-neutral-300);--ghost-hover:#ffffff14;--positive:var(--color-green-950);--positive-foreground:var(--color-green-200);--surface:var(--color-stone-800);--surface-emphasis:var(--color-stone-600);--surface-raised:var(--color-stone-700);--border:var(--color-stone-600);--border-light:var(--color-stone-700);--border-lighter:var(--color-stone-800);--border-strong:var(--color-stone-500);--input:var(--color-stone-800);--input-foreground:var(--color-neutral-50);--ring:var(--color-white);--ring-error:var(--color-red-400);--card:var(--color-stone-800);--card-foreground:var(--color-neutral-100);--popover:var(--color-stone-800);--popover-foreground:var(--color-neutral-100);--warning:var(--color-amber-950);--warning-foreground:var(--color-amber-200);--chart-1:#ff6b33;--chart-2:#00b3a4;--chart-3:#2196a8;--chart-4:#ffc733;--chart-5:#ffad33;--chart-negative:#ff6b33;--chart-positive:#00b3a4;--chart-fill:#3382ff40;--chart-fill-2:#5096ff33;--chart-stroke-2:#5da3ff;--sidebar:var(--color-stone-800);--sidebar-foreground:var(--color-neutral-300);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-stone-700);--sidebar-ring:var(--ring);--backdrop:#000c;--theme-background:#0c0a09}[data-theme=metropolis]{--background:var(--color-white);--background-emphasis:var(--color-neutral-900);--foreground:var(--color-neutral-950);--foreground-alt:var(--color-neutral-700);--foreground-subtle:var(--color-neutral-600);--primary:var(--color-purple-600);--primary-foreground:var(--color-neutral-50);--primary-hover:var(--color-purple-700);--secondary:var(--color-neutral-100);--secondary-foreground:var(--color-neutral-900);--secondary-hover:var(--color-neutral-50);--muted:var(--color-neutral-100);--muted-foreground:var(--color-neutral-500);--accent:var(--color-purple-50);--accent-foreground:var(--color-purple-600);--destructive:var(--color-red-600);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-100);--destructive-soft-foreground:var(--color-red-800);--ghost:#fff0;--ghost-foreground:var(--color-neutral-700);--ghost-hover:#0000000d;--positive:var(--color-green-100);--positive-foreground:var(--color-green-900);--surface:var(--color-neutral-50);--surface-emphasis:var(--color-neutral-300);--surface-raised:var(--color-neutral-200);--border:var(--color-neutral-200);--border-light:var(--color-neutral-100);--border-lighter:var(--color-neutral-50);--border-strong:var(--color-neutral-400);--input:var(--color-neutral-50);--input-foreground:var(--color-neutral-950);--ring:var(--color-neutral-300);--ring-error:var(--color-red-300);--card:var(--color-white);--card-foreground:var(--color-neutral-950);--popover:var(--color-white);--popover-foreground:var(--color-neutral-950);--warning:var(--color-yellow-100);--warning-foreground:var(--color-yellow-900);--chart-1:#f54a00;--chart-2:#009689;--chart-3:#104e64;--chart-4:#ffb900;--chart-5:#fe9a00;--chart-negative:#f54a00;--chart-positive:#009689;--chart-fill:#bfdeffb3;--chart-fill-2:#aaccffb3;--chart-stroke-2:#3f8dff;--sidebar:var(--color-neutral-50);--sidebar-foreground:var(--color-gray-800);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-neutral-200);--sidebar-ring:var(--color-neutral-300);--backdrop:#0009;--theme-background:#f0f3f8}[data-theme=metropolis].dark,[data-theme=metropolis] .dark{--background:var(--color-stone-900);--background-emphasis:var(--color-neutral-100);--foreground:var(--color-neutral-50);--foreground-alt:var(--color-neutral-300);--foreground-subtle:var(--color-neutral-400);--primary:var(--color-purple-500);--primary-foreground:var(--color-white);--primary-hover:var(--color-purple-400);--secondary:var(--color-stone-700);--secondary-foreground:var(--color-neutral-100);--secondary-hover:var(--color-stone-600);--muted:var(--color-stone-800);--muted-foreground:var(--color-neutral-400);--accent:var(--color-purple-900);--accent-foreground:var(--color-purple-300);--destructive:var(--color-red-500);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-950);--destructive-soft-foreground:var(--color-red-200);--ghost:#fff0;--ghost-foreground:var(--color-neutral-300);--ghost-hover:#ffffff14;--positive:var(--color-green-950);--positive-foreground:var(--color-green-200);--surface:var(--color-stone-800);--surface-emphasis:var(--color-stone-600);--surface-raised:var(--color-stone-700);--border:var(--color-stone-600);--border-light:var(--color-stone-700);--border-lighter:var(--color-stone-800);--border-strong:var(--color-stone-500);--input:var(--color-stone-800);--input-foreground:var(--color-neutral-50);--ring:var(--color-neutral-500);--ring-error:var(--color-red-400);--card:var(--color-stone-800);--card-foreground:var(--color-neutral-100);--popover:var(--color-stone-800);--popover-foreground:var(--color-neutral-100);--warning:var(--color-amber-950);--warning-foreground:var(--color-amber-200);--chart-1:#ff6b33;--chart-2:#00b3a4;--chart-3:#2196a8;--chart-4:#ffc733;--chart-5:#ffad33;--chart-negative:#ff6b33;--chart-positive:#00b3a4;--chart-fill:#3382ff40;--chart-fill-2:#5096ff33;--chart-stroke-2:#5da3ff;--sidebar:var(--color-stone-800);--sidebar-foreground:var(--color-neutral-300);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-stone-700);--sidebar-ring:var(--color-neutral-500);--backdrop:#000c;--theme-background:#0c0a09}*{border-color:var(--border)}body{background-color:var(--background);color:var(--foreground)}button,[role=button]{cursor:pointer}:focus-visible{outline:2px solid var(--ring);outline-offset:2px}}@layer components;@layer utilities{.relative{position:relative}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3\\.5{margin-top:calc(var(--spacing) * 3.5)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-px{margin-top:1px}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-1\\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.h-11{height:calc(var(--spacing) * 11)}.min-h-svh{min-height:100svh}.w-full{width:100%}.max-w-sm{max-width:var(--container-sm)}.shrink-0{flex-shrink:0}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\\.5{gap:calc(var(--spacing) * 2.5)}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-border{border-color:var(--color-border)}.border-brand{border-color:var(--color-brand)}.border-destructive{border-color:var(--color-destructive)}.bg-\\[color-mix\\(in_oklab\\,var\\(--destructive\\)_8\\%\\,transparent\\)\\]{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-\\[color-mix\\(in_oklab\\,var\\(--destructive\\)_8\\%\\,transparent\\)\\]{background-color:color-mix(in oklab,var(--destructive) 8%,transparent)}}.bg-\\[color-mix\\(in_oklab\\,var\\(--foreground\\)_6\\%\\,transparent\\)\\]{background-color:var(--foreground)}@supports (color:color-mix(in lab, red, red)){.bg-\\[color-mix\\(in_oklab\\,var\\(--foreground\\)_6\\%\\,transparent\\)\\]{background-color:color-mix(in oklab,var(--foreground) 6%,transparent)}}.bg-\\[var\\(--input\\)\\]{background-color:var(--input)}.bg-brand{background-color:var(--color-brand)}.bg-card{background-color:var(--color-card)}.bg-destructive{background-color:var(--color-destructive)}.bg-primary{background-color:var(--color-primary)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-2\\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.py-px{padding-block:1px}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\\[0\\.85em\\]{font-size:.85em}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.text-\\[var\\(--card\\)\\]{color:var(--card)}.text-brand{color:var(--color-brand)}.text-brand-foreground{color:var(--color-brand-foreground)}.text-destructive{color:var(--color-destructive)}.text-foreground{color:var(--color-foreground)}.text-muted-foreground{color:var(--color-muted-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.shadow-panel{--tw-shadow:var(--panel-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.backdrop-blur-\\[10px\\]{--tw-backdrop-blur:blur(10px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.select-none{-webkit-user-select:none;user-select:none}.canopy-backdrop{background:var(--backdrop)}.paused{animation-play-state:paused}.running{animation-play-state:running}@media (hover:hover){.hover\\:bg-\\[var\\(--primary-hover\\)\\]:hover{background-color:var(--primary-hover)}.hover\\:text-foreground:hover{color:var(--color-foreground)}}.focus-visible\\:border-brand:focus-visible{border-color:var(--color-brand)}.focus-visible\\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\\:outline-offset-\\[3px\\]:focus-visible{outline-offset:3px}.focus-visible\\:outline-brand:focus-visible{outline-color:var(--color-brand)}.aria-invalid\\:border-destructive[aria-invalid=true]{border-color:var(--color-destructive)}}@property --tw-animation-delay{syntax:\"*\";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:\"*\";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:\"*\";inherits:false}@property --tw-animation-fill-mode{syntax:\"*\";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:\"*\";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:\"*\";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:\"*\";inherits:false;initial-value:0}@media (prefers-reduced-motion:reduce){:root{--duration-instant:0s;--duration-fast:0s;--duration-base:0s;--duration-slow:0s;--duration-slower:0s;--default-transition-duration:0s}[data-vaul-drawer],[data-vaul-overlay]{transition-duration:0s!important;animation-duration:0s!important}}.dark{--input:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--input:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}.dark{--surface:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--surface:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}.dark{--muted:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--muted:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}:root:not(.dark){--background:var(--color-neutral-50)}[data-slot=dialog-content],[data-slot=alert-dialog-content],[data-slot=sheet-content],[data-slot=drawer-content]{background-color:var(--card)}[data-slot=table-body] tr:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){[data-slot=table-body] tr:hover{background-color:color-mix(in oklab, var(--accent) 45%, transparent)}}:root{--brand:#2f6b4f;--brand-foreground:#f4faf6;--foreground:#14201a;--muted-foreground:#4a5c52;--border:#d5ddd7;--card:#ffffffd1;--input:#ffffffe6;--primary:#14201a;--primary-foreground:#f4f7f5;--primary-hover:var(--primary)}@supports (color:color-mix(in lab, red, red)){:root{--primary-hover:color-mix(in oklab, var(--primary) 72%, var(--brand))}}:root{--destructive:#a23b28;--ring:var(--brand);--panel-shadow:0 18px 40px -28px #14201a73;--backdrop:radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%), radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%), linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef)}.dark{--brand:#7cc39d;--brand-foreground:#0d1410;--foreground:#e9f0ec;--muted-foreground:#a2b3aa;--border:#e9f0ec24;--card:#181f1bb8;--input:#e9f0ec0f;--primary:#e9f0ec;--primary-foreground:#101711;--primary-hover:var(--primary)}@supports (color:color-mix(in lab, red, red)){.dark{--primary-hover:color-mix(in oklab, var(--primary) 72%, var(--brand))}}.dark{--destructive:#f0a596;--panel-shadow:0 18px 44px -26px #000000b3;--backdrop:radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%), radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%), linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f)}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-tracking{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:\"*\";inherits:false}@property --tw-backdrop-brightness{syntax:\"*\";inherits:false}@property --tw-backdrop-contrast{syntax:\"*\";inherits:false}@property --tw-backdrop-grayscale{syntax:\"*\";inherits:false}@property --tw-backdrop-hue-rotate{syntax:\"*\";inherits:false}@property --tw-backdrop-invert{syntax:\"*\";inherits:false}@property --tw-backdrop-opacity{syntax:\"*\";inherits:false}@property --tw-backdrop-saturate{syntax:\"*\";inherits:false}@property --tw-backdrop-sepia{syntax:\"*\";inherits:false}@property --tw-duration{syntax:\"*\";inherits:false}@property --tw-outline-style{syntax:\"*\";inherits:false;initial-value:solid}</style><script>matchMedia('(prefers-color-scheme:dark)').matches&&document.documentElement.classList.add('dark')<\/script></head><body class=\"canopy-backdrop grid min-h-svh place-items-center p-6 text-foreground\"><main class=\"w-full max-w-sm rounded-2xl border border-border bg-card p-8 shadow-panel backdrop-blur-[10px]\"><div class=\"mb-5 grid size-11 place-items-center rounded-xl bg-brand text-brand-foreground\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-check size-5\" aria-hidden=\"true\"><path d=\"M20 6 9 17l-5-5\"></path></svg></div><p class=\"mb-1 text-xs font-semibold uppercase tracking-wide text-brand\">Canopy CLI</p><h1 class=\"mb-2 text-2xl font-semibold tracking-tight break-words\">You’re signed in</h1><p class=\"mt-2 text-sm leading-relaxed text-muted-foreground\">You can close this tab and return to the terminal — the CLI has your code.</p><p class=\"mt-6 border-t border-border pt-4 text-xs text-muted-foreground\">This tab only finishes the browser step — keep using the terminal for the rest.</p></main></body></html>";
9
+ const failedHtml = "<!doctype html><html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>Sign-in didn’t finish · Canopy</title><style>/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */\n@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-outline-style:solid;--tw-animation-delay:0s;--tw-animation-direction:normal;--tw-animation-duration:initial;--tw-animation-fill-mode:none;--tw-animation-iteration-count:1;--tw-enter-blur:0;--tw-enter-opacity:1;--tw-enter-rotate:0;--tw-enter-scale:1;--tw-enter-translate-x:0;--tw-enter-translate-y:0;--tw-exit-blur:0;--tw-exit-opacity:1;--tw-exit-rotate:0;--tw-exit-scale:1;--tw-exit-translate-x:0;--tw-exit-translate-y:0}}}@layer theme{:root,:host{--font-sans:\"Helvetica Now for Monks\", ui-sans-serif, system-ui, sans-serif;--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;--color-red-100:#ffebf0;--color-red-200:#ffd1dd;--color-red-300:#ffa8be;--color-red-400:#ff7a9c;--color-red-500:#ff527d;--color-red-600:#ff245b;--color-red-800:#a30029;--color-red-950:#610018;--color-amber-200:#fde68a;--color-amber-950:#451a03;--color-yellow-100:#fff7db;--color-yellow-900:#7a6000;--color-green-100:#e3fcf1;--color-green-200:#b8fadd;--color-green-900:#067445;--color-green-950:#065533;--color-purple-50:#faf6fd;--color-purple-300:#d6b6f1;--color-purple-400:#c598ec;--color-purple-500:#ac6de3;--color-purple-600:#8c3fd2;--color-purple-700:#7d26c9;--color-purple-900:#501881;--color-gray-800:#5d5d60;--color-neutral-50:#f5f5f4;--color-neutral-100:#e9e8e7;--color-neutral-200:#d6d4d2;--color-neutral-300:#c0beb9;--color-neutral-400:#adaaa4;--color-neutral-500:#97938c;--color-neutral-600:#837f77;--color-neutral-700:#6c6962;--color-neutral-900:#484641;--color-neutral-950:#3e3c38;--color-stone-500:#78716c;--color-stone-600:#57534e;--color-stone-700:#44403c;--color-stone-800:#292524;--color-stone-900:#1c1917;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-sm:24rem;--text-xs:12px;--text-xs--line-height:16px;--text-sm:14px;--text-sm--line-height:20px;--text-2xl:24px;--text-2xl--line-height:32px;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.01em;--tracking-wide:.015em;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--radius-md:6px;--radius-lg:8px;--radius-xl:12px;--radius-2xl:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-foreground:var(--foreground);--color-primary:var(--primary);--color-primary-foreground:var(--primary-foreground);--color-muted-foreground:var(--muted-foreground);--color-destructive:var(--destructive);--color-border:var(--border);--color-card:var(--card);--color-brand:var(--brand);--color-brand-foreground:var(--brand-foreground)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", \"Noto Sans\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root,[data-theme=monks-flow]{--background:var(--color-white);--background-emphasis:var(--color-neutral-900);--foreground:var(--color-neutral-950);--foreground-alt:var(--color-neutral-700);--foreground-subtle:var(--color-neutral-600);--primary:var(--color-neutral-900);--primary-foreground:var(--color-neutral-50);--primary-hover:var(--color-neutral-950);--secondary:#f5f3f0;--secondary-foreground:var(--color-neutral-900);--secondary-hover:#e7e2dc;--muted:var(--color-neutral-100);--muted-foreground:var(--color-neutral-500);--accent:#e7e2dc;--accent-foreground:var(--color-neutral-900);--destructive:#e11d48;--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-100);--destructive-soft-foreground:var(--color-red-800);--ghost:#fff0;--ghost-foreground:var(--color-neutral-700);--ghost-hover:#0000000d;--positive:var(--color-green-100);--positive-foreground:var(--color-green-900);--surface:var(--color-neutral-50);--surface-emphasis:var(--color-neutral-300);--surface-raised:var(--color-neutral-200);--border:var(--color-neutral-200);--border-light:var(--color-neutral-100);--border-lighter:var(--color-neutral-50);--border-strong:var(--color-neutral-400);--input:var(--color-neutral-50);--input-foreground:var(--color-neutral-950);--ring:var(--color-black);--ring-error:var(--color-red-300);--card:var(--color-white);--card-foreground:var(--color-neutral-950);--popover:var(--color-white);--popover-foreground:var(--color-neutral-950);--warning:var(--color-yellow-100);--warning-foreground:var(--color-yellow-900);--chart-1:#f54a00;--chart-2:#009689;--chart-3:#104e64;--chart-4:#ffb900;--chart-5:#fe9a00;--chart-negative:#f54a00;--chart-positive:#009689;--chart-fill:#bfdeffb3;--chart-fill-2:#aaccffb3;--chart-stroke-2:#3f8dff;--sidebar:var(--color-neutral-50);--sidebar-foreground:var(--color-gray-800);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-neutral-200);--sidebar-ring:var(--ring);--backdrop:#0009;--theme-background:#f5f4f2}:root.dark:not([data-theme]),[data-theme=monks-flow].dark,[data-theme=monks-flow] .dark{--background:var(--color-stone-900);--background-emphasis:var(--color-neutral-100);--foreground:var(--color-neutral-50);--foreground-alt:var(--color-neutral-300);--foreground-subtle:var(--color-neutral-400);--primary:var(--color-neutral-50);--primary-foreground:var(--color-black);--primary-hover:var(--color-neutral-100);--secondary:var(--color-stone-700);--secondary-foreground:var(--color-neutral-100);--secondary-hover:var(--color-stone-600);--muted:var(--color-stone-800);--muted-foreground:var(--color-neutral-500);--accent:var(--color-neutral-900);--accent-foreground:var(--color-neutral-300);--destructive:var(--color-red-500);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-950);--destructive-soft-foreground:var(--color-red-200);--ghost:#fff0;--ghost-foreground:var(--color-neutral-300);--ghost-hover:#ffffff14;--positive:var(--color-green-950);--positive-foreground:var(--color-green-200);--surface:var(--color-stone-800);--surface-emphasis:var(--color-stone-600);--surface-raised:var(--color-stone-700);--border:var(--color-stone-600);--border-light:var(--color-stone-700);--border-lighter:var(--color-stone-800);--border-strong:var(--color-stone-500);--input:var(--color-stone-800);--input-foreground:var(--color-neutral-50);--ring:var(--color-white);--ring-error:var(--color-red-400);--card:var(--color-stone-800);--card-foreground:var(--color-neutral-100);--popover:var(--color-stone-800);--popover-foreground:var(--color-neutral-100);--warning:var(--color-amber-950);--warning-foreground:var(--color-amber-200);--chart-1:#ff6b33;--chart-2:#00b3a4;--chart-3:#2196a8;--chart-4:#ffc733;--chart-5:#ffad33;--chart-negative:#ff6b33;--chart-positive:#00b3a4;--chart-fill:#3382ff40;--chart-fill-2:#5096ff33;--chart-stroke-2:#5da3ff;--sidebar:var(--color-stone-800);--sidebar-foreground:var(--color-neutral-300);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-stone-700);--sidebar-ring:var(--ring);--backdrop:#000c;--theme-background:#0c0a09}[data-theme=metropolis]{--background:var(--color-white);--background-emphasis:var(--color-neutral-900);--foreground:var(--color-neutral-950);--foreground-alt:var(--color-neutral-700);--foreground-subtle:var(--color-neutral-600);--primary:var(--color-purple-600);--primary-foreground:var(--color-neutral-50);--primary-hover:var(--color-purple-700);--secondary:var(--color-neutral-100);--secondary-foreground:var(--color-neutral-900);--secondary-hover:var(--color-neutral-50);--muted:var(--color-neutral-100);--muted-foreground:var(--color-neutral-500);--accent:var(--color-purple-50);--accent-foreground:var(--color-purple-600);--destructive:var(--color-red-600);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-100);--destructive-soft-foreground:var(--color-red-800);--ghost:#fff0;--ghost-foreground:var(--color-neutral-700);--ghost-hover:#0000000d;--positive:var(--color-green-100);--positive-foreground:var(--color-green-900);--surface:var(--color-neutral-50);--surface-emphasis:var(--color-neutral-300);--surface-raised:var(--color-neutral-200);--border:var(--color-neutral-200);--border-light:var(--color-neutral-100);--border-lighter:var(--color-neutral-50);--border-strong:var(--color-neutral-400);--input:var(--color-neutral-50);--input-foreground:var(--color-neutral-950);--ring:var(--color-neutral-300);--ring-error:var(--color-red-300);--card:var(--color-white);--card-foreground:var(--color-neutral-950);--popover:var(--color-white);--popover-foreground:var(--color-neutral-950);--warning:var(--color-yellow-100);--warning-foreground:var(--color-yellow-900);--chart-1:#f54a00;--chart-2:#009689;--chart-3:#104e64;--chart-4:#ffb900;--chart-5:#fe9a00;--chart-negative:#f54a00;--chart-positive:#009689;--chart-fill:#bfdeffb3;--chart-fill-2:#aaccffb3;--chart-stroke-2:#3f8dff;--sidebar:var(--color-neutral-50);--sidebar-foreground:var(--color-gray-800);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-neutral-200);--sidebar-ring:var(--color-neutral-300);--backdrop:#0009;--theme-background:#f0f3f8}[data-theme=metropolis].dark,[data-theme=metropolis] .dark{--background:var(--color-stone-900);--background-emphasis:var(--color-neutral-100);--foreground:var(--color-neutral-50);--foreground-alt:var(--color-neutral-300);--foreground-subtle:var(--color-neutral-400);--primary:var(--color-purple-500);--primary-foreground:var(--color-white);--primary-hover:var(--color-purple-400);--secondary:var(--color-stone-700);--secondary-foreground:var(--color-neutral-100);--secondary-hover:var(--color-stone-600);--muted:var(--color-stone-800);--muted-foreground:var(--color-neutral-400);--accent:var(--color-purple-900);--accent-foreground:var(--color-purple-300);--destructive:var(--color-red-500);--destructive-foreground:var(--color-white);--destructive-soft:var(--color-red-950);--destructive-soft-foreground:var(--color-red-200);--ghost:#fff0;--ghost-foreground:var(--color-neutral-300);--ghost-hover:#ffffff14;--positive:var(--color-green-950);--positive-foreground:var(--color-green-200);--surface:var(--color-stone-800);--surface-emphasis:var(--color-stone-600);--surface-raised:var(--color-stone-700);--border:var(--color-stone-600);--border-light:var(--color-stone-700);--border-lighter:var(--color-stone-800);--border-strong:var(--color-stone-500);--input:var(--color-stone-800);--input-foreground:var(--color-neutral-50);--ring:var(--color-neutral-500);--ring-error:var(--color-red-400);--card:var(--color-stone-800);--card-foreground:var(--color-neutral-100);--popover:var(--color-stone-800);--popover-foreground:var(--color-neutral-100);--warning:var(--color-amber-950);--warning-foreground:var(--color-amber-200);--chart-1:#ff6b33;--chart-2:#00b3a4;--chart-3:#2196a8;--chart-4:#ffc733;--chart-5:#ffad33;--chart-negative:#ff6b33;--chart-positive:#00b3a4;--chart-fill:#3382ff40;--chart-fill-2:#5096ff33;--chart-stroke-2:#5da3ff;--sidebar:var(--color-stone-800);--sidebar-foreground:var(--color-neutral-300);--sidebar-primary:var(--primary);--sidebar-primary-foreground:var(--primary-foreground);--sidebar-accent:var(--accent);--sidebar-accent-foreground:var(--accent-foreground);--sidebar-border:var(--color-stone-700);--sidebar-ring:var(--color-neutral-500);--backdrop:#000c;--theme-background:#0c0a09}*{border-color:var(--border)}body{background-color:var(--background);color:var(--foreground)}button,[role=button]{cursor:pointer}:focus-visible{outline:2px solid var(--ring);outline-offset:2px}}@layer components;@layer utilities{.relative{position:relative}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3\\.5{margin-top:calc(var(--spacing) * 3.5)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-px{margin-top:1px}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:var(--spacing)}.mb-1\\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.h-11{height:calc(var(--spacing) * 11)}.min-h-svh{min-height:100svh}.w-full{width:100%}.max-w-sm{max-width:var(--container-sm)}.shrink-0{flex-shrink:0}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-center{justify-content:center}.gap-1\\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\\.5{gap:calc(var(--spacing) * 2.5)}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-r-md{border-top-right-radius:var(--radius-md);border-bottom-right-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-border{border-color:var(--color-border)}.border-brand{border-color:var(--color-brand)}.border-destructive{border-color:var(--color-destructive)}.bg-\\[color-mix\\(in_oklab\\,var\\(--destructive\\)_8\\%\\,transparent\\)\\]{background-color:var(--destructive)}@supports (color:color-mix(in lab, red, red)){.bg-\\[color-mix\\(in_oklab\\,var\\(--destructive\\)_8\\%\\,transparent\\)\\]{background-color:color-mix(in oklab,var(--destructive) 8%,transparent)}}.bg-\\[color-mix\\(in_oklab\\,var\\(--foreground\\)_6\\%\\,transparent\\)\\]{background-color:var(--foreground)}@supports (color:color-mix(in lab, red, red)){.bg-\\[color-mix\\(in_oklab\\,var\\(--foreground\\)_6\\%\\,transparent\\)\\]{background-color:color-mix(in oklab,var(--foreground) 6%,transparent)}}.bg-\\[var\\(--input\\)\\]{background-color:var(--input)}.bg-brand{background-color:var(--color-brand)}.bg-card{background-color:var(--color-card)}.bg-destructive{background-color:var(--color-destructive)}.bg-primary{background-color:var(--color-primary)}.p-6{padding:calc(var(--spacing) * 6)}.p-8{padding:calc(var(--spacing) * 8)}.px-1{padding-inline:var(--spacing)}.px-2\\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3\\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\\.5{padding-block:calc(var(--spacing) * 2.5)}.py-px{padding-block:1px}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pl-3{padding-left:calc(var(--spacing) * 3)}.text-center{text-align:center}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\\[0\\.85em\\]{font-size:.85em}.leading-normal{--tw-leading:var(--leading-normal);line-height:var(--leading-normal)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.break-words{overflow-wrap:break-word}.text-\\[var\\(--card\\)\\]{color:var(--card)}.text-brand{color:var(--color-brand)}.text-brand-foreground{color:var(--color-brand-foreground)}.text-destructive{color:var(--color-destructive)}.text-foreground{color:var(--color-foreground)}.text-muted-foreground{color:var(--color-muted-foreground)}.text-primary-foreground{color:var(--color-primary-foreground)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.shadow-panel{--tw-shadow:var(--panel-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.backdrop-blur-\\[10px\\]{--tw-backdrop-blur:blur(10px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.select-none{-webkit-user-select:none;user-select:none}.canopy-backdrop{background:var(--backdrop)}.paused{animation-play-state:paused}.running{animation-play-state:running}@media (hover:hover){.hover\\:bg-\\[var\\(--primary-hover\\)\\]:hover{background-color:var(--primary-hover)}.hover\\:text-foreground:hover{color:var(--color-foreground)}}.focus-visible\\:border-brand:focus-visible{border-color:var(--color-brand)}.focus-visible\\:outline-2:focus-visible{outline-style:var(--tw-outline-style);outline-width:2px}.focus-visible\\:outline-offset-1:focus-visible{outline-offset:1px}.focus-visible\\:outline-offset-2:focus-visible{outline-offset:2px}.focus-visible\\:outline-offset-\\[3px\\]:focus-visible{outline-offset:3px}.focus-visible\\:outline-brand:focus-visible{outline-color:var(--color-brand)}.aria-invalid\\:border-destructive[aria-invalid=true]{border-color:var(--color-destructive)}}@property --tw-animation-delay{syntax:\"*\";inherits:false;initial-value:0s}@property --tw-animation-direction{syntax:\"*\";inherits:false;initial-value:normal}@property --tw-animation-duration{syntax:\"*\";inherits:false}@property --tw-animation-fill-mode{syntax:\"*\";inherits:false;initial-value:none}@property --tw-animation-iteration-count{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-blur{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-opacity{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-rotate{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-scale{syntax:\"*\";inherits:false;initial-value:1}@property --tw-enter-translate-x{syntax:\"*\";inherits:false;initial-value:0}@property --tw-enter-translate-y{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-blur{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-opacity{syntax:\"*\";inherits:false;initial-value:1}@property --tw-exit-rotate{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-scale{syntax:\"*\";inherits:false;initial-value:1}@property --tw-exit-translate-x{syntax:\"*\";inherits:false;initial-value:0}@property --tw-exit-translate-y{syntax:\"*\";inherits:false;initial-value:0}@media (prefers-reduced-motion:reduce){:root{--duration-instant:0s;--duration-fast:0s;--duration-base:0s;--duration-slow:0s;--duration-slower:0s;--default-transition-duration:0s}[data-vaul-drawer],[data-vaul-overlay]{transition-duration:0s!important;animation-duration:0s!important}}.dark{--input:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--input:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}.dark{--surface:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--surface:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}.dark{--muted:#35312f}@supports (color:color-mix(in lab, red, red)){.dark{--muted:color-mix(in oklab, var(--color-stone-800) 55%, var(--color-stone-700))}}:root:not(.dark){--background:var(--color-neutral-50)}[data-slot=dialog-content],[data-slot=alert-dialog-content],[data-slot=sheet-content],[data-slot=drawer-content]{background-color:var(--card)}[data-slot=table-body] tr:hover{background-color:var(--accent)}@supports (color:color-mix(in lab, red, red)){[data-slot=table-body] tr:hover{background-color:color-mix(in oklab, var(--accent) 45%, transparent)}}:root{--brand:#2f6b4f;--brand-foreground:#f4faf6;--foreground:#14201a;--muted-foreground:#4a5c52;--border:#d5ddd7;--card:#ffffffd1;--input:#ffffffe6;--primary:#14201a;--primary-foreground:#f4f7f5;--primary-hover:var(--primary)}@supports (color:color-mix(in lab, red, red)){:root{--primary-hover:color-mix(in oklab, var(--primary) 72%, var(--brand))}}:root{--destructive:#a23b28;--ring:var(--brand);--panel-shadow:0 18px 40px -28px #14201a73;--backdrop:radial-gradient(900px 480px at 12% -10%, #dfece3, transparent 60%), radial-gradient(700px 420px at 100% 0%, #e7efe4, transparent 55%), linear-gradient(165deg, #e8f0ea, #f7f5f0 55%, #eef2ef)}.dark{--brand:#7cc39d;--brand-foreground:#0d1410;--foreground:#e9f0ec;--muted-foreground:#a2b3aa;--border:#e9f0ec24;--card:#181f1bb8;--input:#e9f0ec0f;--primary:#e9f0ec;--primary-foreground:#101711;--primary-hover:var(--primary)}@supports (color:color-mix(in lab, red, red)){.dark{--primary-hover:color-mix(in oklab, var(--primary) 72%, var(--brand))}}.dark{--destructive:#f0a596;--panel-shadow:0 18px 44px -26px #000000b3;--backdrop:radial-gradient(900px 480px at 12% -10%, #16261d, transparent 60%), radial-gradient(700px 420px at 100% 0%, #16221c, transparent 55%), linear-gradient(165deg, #0e1512, #121a15 55%, #0d130f)}@property --tw-border-style{syntax:\"*\";inherits:false;initial-value:solid}@property --tw-leading{syntax:\"*\";inherits:false}@property --tw-font-weight{syntax:\"*\";inherits:false}@property --tw-tracking{syntax:\"*\";inherits:false}@property --tw-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:\"*\";inherits:false}@property --tw-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:\"*\";inherits:false}@property --tw-inset-shadow-alpha{syntax:\"<percentage>\";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:\"*\";inherits:false}@property --tw-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:\"*\";inherits:false}@property --tw-inset-ring-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:\"*\";inherits:false}@property --tw-ring-offset-width{syntax:\"<length>\";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:\"*\";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:\"*\";inherits:false;initial-value:0 0 #0000}@property --tw-backdrop-blur{syntax:\"*\";inherits:false}@property --tw-backdrop-brightness{syntax:\"*\";inherits:false}@property --tw-backdrop-contrast{syntax:\"*\";inherits:false}@property --tw-backdrop-grayscale{syntax:\"*\";inherits:false}@property --tw-backdrop-hue-rotate{syntax:\"*\";inherits:false}@property --tw-backdrop-invert{syntax:\"*\";inherits:false}@property --tw-backdrop-opacity{syntax:\"*\";inherits:false}@property --tw-backdrop-saturate{syntax:\"*\";inherits:false}@property --tw-backdrop-sepia{syntax:\"*\";inherits:false}@property --tw-duration{syntax:\"*\";inherits:false}@property --tw-outline-style{syntax:\"*\";inherits:false;initial-value:solid}</style><script>matchMedia('(prefers-color-scheme:dark)').matches&&document.documentElement.classList.add('dark')<\/script></head><body class=\"canopy-backdrop grid min-h-svh place-items-center p-6 text-foreground\"><main class=\"w-full max-w-sm rounded-2xl border border-border bg-card p-8 shadow-panel backdrop-blur-[10px]\"><div class=\"mb-5 grid size-11 place-items-center rounded-xl bg-destructive text-[var(--card)]\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"lucide lucide-circle-alert size-5\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"10\"></circle><line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\"></line><line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\"></line></svg></div><p class=\"mb-1 text-xs font-semibold uppercase tracking-wide text-brand\">Canopy CLI</p><h1 class=\"mb-2 text-2xl font-semibold tracking-tight break-words\">Sign-in didn’t finish</h1><p class=\"mt-2 text-sm leading-relaxed text-muted-foreground\">Return to the terminal and run canopy login again. Reloading this page won’t help.</p><p class=\"mt-6 border-t border-border pt-4 text-xs text-muted-foreground\">If the terminal still says it’s waiting, cancel with <kbd class=\"rounded border border-border bg-[var(--input)] px-1 py-px text-[0.85em] font-normal\">Ctrl</kbd>+<kbd class=\"rounded border border-border bg-[var(--input)] px-1 py-px text-[0.85em] font-normal\">C</kbd> and try again.</p></main></body></html>";
10
+ //#endregion
11
+ //#region src/login-page.ts
12
+ /**
13
+ * Branded HTML for the local OneLogin PKCE callback (127.0.0.1:8976). The page
14
+ * lives in @mediamonks/canopy-pages next to the gateway's sign-in and error
15
+ * walls so all seven look like one another, and is pre-rendered there at build
16
+ * time: it takes no data, and the CLI has no business shipping React and an
17
+ * icon set to print two fixed documents.
18
+ */
19
+ function loginCallbackPage(ok) {
20
+ return ok ? okHtml : failedHtml;
21
+ }
22
+ //#endregion
23
+ //#region src/login.ts
24
+ /**
25
+ * canopy login | whoami | logout — browser PKCE flow against OneLogin (public
26
+ * client), then trade the ID token for platform CLI tokens via POST
27
+ * /control/v1/auth/cli-token on every baked site (docs/10-cli-sites.md D17).
28
+ */
29
+ const CALLBACK_PORT = 8976;
30
+ const REDIRECT_URI = `http://127.0.0.1:${CALLBACK_PORT}/callback`;
31
+ const LOGIN_TIMEOUT_MS = 300 * 1e3;
32
+ const base64url = (buf) => buf.toString("base64url");
33
+ const whoamiCommand = defineCommand({
34
+ meta: {
35
+ name: "whoami",
36
+ description: "Show the current Canopy login"
37
+ },
38
+ run: reportErrors("whoami", async () => {
39
+ const config = siteConfig();
40
+ if (!config.token) throw new Error("Not logged in — run `canopy login`.");
41
+ console.log(config.email ?? "<unknown email>");
42
+ if (config.cloud) console.log(`Cloud: ${config.cloud}`);
43
+ console.log(`API: ${config.apiUrl}`);
44
+ if (config.tokenExpiresAt !== void 0) {
45
+ const expired = Date.parse(config.tokenExpiresAt) < Date.now();
46
+ console.log(expired ? `Token EXPIRED ${config.tokenExpiresAt} — run \`canopy login\` again.` : `Token expires ${config.tokenExpiresAt}.`);
47
+ }
48
+ })
49
+ });
50
+ const logoutCommand = defineCommand({
51
+ meta: {
52
+ name: "logout",
53
+ description: "Sign out: forget all local CLI tokens"
54
+ },
55
+ run: reportErrors("logout", async () => {
56
+ const file = readConfigFile();
57
+ if (!Object.values(file.profiles ?? {}).some((p) => p?.token)) {
58
+ console.log("Not logged in.");
59
+ return;
60
+ }
61
+ clearAllProfileTokens();
62
+ console.log("Logged out — all site tokens removed from ~/.canopy/config.json.");
63
+ })
64
+ });
65
+ /** Wait for OneLogin to redirect back to the local loopback server. */
66
+ function waitForCallback(expectedState) {
67
+ return new Promise((resolve, reject) => {
68
+ const server = createServer((req, res) => {
69
+ const url = new URL(req.url ?? "/", REDIRECT_URI);
70
+ if (url.pathname !== "/callback") {
71
+ res.writeHead(404).end();
72
+ return;
73
+ }
74
+ const code = url.searchParams.get("code");
75
+ const state = url.searchParams.get("state");
76
+ const ok = Boolean(code) && state === expectedState;
77
+ res.writeHead(ok ? 200 : 400, {
78
+ "content-type": "text/html; charset=utf-8",
79
+ "cache-control": "no-store"
80
+ }).end(loginCallbackPage(ok));
81
+ server.close();
82
+ clearTimeout(timer);
83
+ if (ok) resolve(code);
84
+ else reject(new Error(url.searchParams.get("error_description") ?? "Login rejected"));
85
+ });
86
+ const timer = setTimeout(() => {
87
+ server.close();
88
+ reject(/* @__PURE__ */ new Error("Timed out waiting for the browser login (5 minutes)."));
89
+ }, LOGIN_TIMEOUT_MS);
90
+ 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));
91
+ server.listen(CALLBACK_PORT, "127.0.0.1");
92
+ });
93
+ }
94
+ async function pickActiveProfile(json, flag) {
95
+ if (flag) {
96
+ if (!isProfileId(flag)) throw new Error(`Unknown profile "${flag}". Choose one of: ${listBakedProfiles().map((p) => p.id).join(", ")}.`);
97
+ return flag;
98
+ }
99
+ const env = process.env["CANOPY_PROFILE"]?.trim();
100
+ if (env) {
101
+ if (!isProfileId(env)) throw new Error(`Unknown CANOPY_PROFILE "${env}".`);
102
+ return env;
103
+ }
104
+ const profiles = listBakedProfiles();
105
+ 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(", ")}.`);
106
+ const choice = await p.select({
107
+ message: "Which Canopy site should be your default?",
108
+ options: profiles.map((prof) => ({
109
+ value: prof.id,
110
+ label: prof.label,
111
+ hint: prof.cloud
112
+ })),
113
+ initialValue: "main"
114
+ });
115
+ if (p.isCancel(choice)) {
116
+ p.cancel("Login cancelled.");
117
+ process.exit(1);
118
+ }
119
+ p.note(`Using profile "${choice}". Agents/non-TTY: canopy login --profile ${choice}`, "Active profile");
120
+ return choice;
121
+ }
122
+ async function mintForProfile(id, idToken) {
123
+ const baked = getBakedProfile(id);
124
+ try {
125
+ const cliTokenRes = await fetch(`${baked.apiUrl}/control/v1/auth/cli-token`, {
126
+ method: "POST",
127
+ headers: { "content-type": "application/json" },
128
+ body: JSON.stringify({ id_token: idToken })
129
+ });
130
+ if (!cliTokenRes.ok) return {
131
+ ok: false,
132
+ error: `${cliTokenRes.status} ${await cliTokenRes.text()}`
133
+ };
134
+ const { token, expires_at, user } = await cliTokenRes.json();
135
+ upsertProfileAuth(id, {
136
+ token,
137
+ tokenExpiresAt: expires_at,
138
+ email: user.email
139
+ });
140
+ return {
141
+ ok: true,
142
+ email: user.email,
143
+ expiresAt: expires_at
144
+ };
145
+ } catch (err) {
146
+ return {
147
+ ok: false,
148
+ error: err instanceof Error ? err.message : String(err)
149
+ };
150
+ }
151
+ }
152
+ async function login(json, profileFlag) {
153
+ if (!json) p.intro("canopy login");
154
+ const active = await pickActiveProfile(json, profileFlag);
155
+ const file = readConfigFile();
156
+ const { issuer, cliClientId } = {
157
+ issuer: process.env["CANOPY_OIDC_ISSUER"] ?? file.issuer ?? "https://s4capital.onelogin.com/oidc/2",
158
+ cliClientId: process.env["CANOPY_OIDC_CLIENT_ID"] ?? file.cliClientId ?? "b69755d0-6678-013f-b5ce-09fe44a57aa0180501"
159
+ };
160
+ 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).");
161
+ writeConfigFile({
162
+ ...file,
163
+ issuer,
164
+ cliClientId,
165
+ activeProfile: active
166
+ });
167
+ const discoveryRes = await fetch(`${issuer}/.well-known/openid-configuration`);
168
+ if (!discoveryRes.ok) throw new Error(`OIDC discovery failed: ${discoveryRes.status}`);
169
+ const discovery = await discoveryRes.json();
170
+ const state = base64url(randomBytes(32));
171
+ const codeVerifier = base64url(randomBytes(32));
172
+ const authorizeUrl = new URL(discovery.authorization_endpoint);
173
+ authorizeUrl.search = new URLSearchParams({
174
+ response_type: "code",
175
+ client_id: cliClientId,
176
+ redirect_uri: REDIRECT_URI,
177
+ scope: "openid profile email",
178
+ state,
179
+ code_challenge: base64url(createHash("sha256").update(codeVerifier).digest()),
180
+ code_challenge_method: "S256"
181
+ }).toString();
182
+ const authorizeHref = authorizeUrl.toString();
183
+ openBrowser(authorizeHref);
184
+ const spin = json ? void 0 : p.spinner();
185
+ if (json) console.log(JSON.stringify({
186
+ status: "waiting",
187
+ url: authorizeHref
188
+ }));
189
+ else {
190
+ p.log.step("Opening OneLogin in your browser");
191
+ p.log.message("If nothing opens, visit:");
192
+ console.log(authorizeHref);
193
+ spin?.start("Waiting for you to sign in in the browser…");
194
+ }
195
+ let code;
196
+ try {
197
+ code = await waitForCallback(state);
198
+ } catch (err) {
199
+ spin?.error("Sign-in failed.");
200
+ throw err;
201
+ }
202
+ spin?.stop("Signed in with OneLogin.");
203
+ const tokenRes = await fetch(discovery.token_endpoint, {
204
+ method: "POST",
205
+ headers: { "content-type": "application/x-www-form-urlencoded" },
206
+ body: new URLSearchParams({
207
+ grant_type: "authorization_code",
208
+ client_id: cliClientId,
209
+ code,
210
+ redirect_uri: REDIRECT_URI,
211
+ code_verifier: codeVerifier
212
+ })
213
+ });
214
+ if (!tokenRes.ok) throw new Error(`Token exchange failed: ${tokenRes.status} ${await tokenRes.text()}`);
215
+ const { id_token: idToken } = await tokenRes.json();
216
+ if (!idToken) throw new Error("OneLogin returned no id_token.");
217
+ const mintSpin = json ? void 0 : p.spinner();
218
+ mintSpin?.start("Minting CLI tokens for each site…");
219
+ const results = [];
220
+ for (const prof of listBakedProfiles()) {
221
+ const result = await mintForProfile(prof.id, idToken);
222
+ results.push({
223
+ id: prof.id,
224
+ ok: result.ok,
225
+ detail: result.ok ? `expires ${result.expiresAt}` : result.error
226
+ });
227
+ }
228
+ mintSpin?.stop("Token mint finished.");
229
+ if (!results.find((r) => r.id === active)?.ok) {
230
+ const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : "✗"}`).join(" ");
231
+ throw new Error(`Could not mint a token for active profile "${active}". ${summary}. Re-run \`canopy login\`.`);
232
+ }
233
+ setActiveProfile(active);
234
+ const email = readConfigFile().profiles?.[active]?.email;
235
+ const summary = results.map((r) => `${r.id} ${r.ok ? "✓" : `✗ (${r.detail})`}`).join("\n");
236
+ if (json) console.log(JSON.stringify({
237
+ status: "ok",
238
+ email,
239
+ activeProfile: active,
240
+ profiles: Object.fromEntries(results.map((r) => [r.id, {
241
+ ok: r.ok,
242
+ detail: r.detail
243
+ }]))
244
+ }));
245
+ else {
246
+ p.note(summary, "Sites");
247
+ p.outro(`Logged in as ${email ?? "unknown"} — default profile "${active}".`);
248
+ for (const r of results.filter((x) => !x.ok)) p.log.warn(`${r.id} failed — re-run \`canopy login\` to retry all sites.`);
249
+ }
250
+ }
251
+ const loginCommand = defineCommand({
252
+ meta: {
253
+ name: "login",
254
+ description: "Sign in to Canopy via OneLogin"
255
+ },
256
+ args: {
257
+ json: {
258
+ type: "boolean",
259
+ default: false,
260
+ description: "Emit machine-readable status to stdout (for agents/CI)"
261
+ },
262
+ profile: {
263
+ type: "string",
264
+ description: "Active default profile (main | staging | gcp-pilot); required non-TTY"
265
+ }
266
+ },
267
+ run: reportErrors("login", async ({ args }) => {
268
+ const json = args.json;
269
+ try {
270
+ await login(json, args.profile);
271
+ } catch (err) {
272
+ if (json) {
273
+ console.log(JSON.stringify({
274
+ status: "error",
275
+ message: err instanceof Error ? err.message : String(err)
276
+ }));
277
+ process.exitCode = 1;
278
+ return;
279
+ }
280
+ throw err;
281
+ }
282
+ })
283
+ });
284
+ //#endregion
285
+ export { loginCommand, logoutCommand, whoamiCommand };
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.19";
2
+ var version = "0.7.20";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-qe7a3VEV.mjs";
1
+ import { t as version } from "./package-B-aXVcQu.mjs";
2
2
  import { r as reportErrors } from "./ui--atbZ-TM.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
112
112
  const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
113
113
  const command = pmCommand(pm);
114
114
  const cmd = installArgs(pm, spec);
115
- const { skillInstalled } = await import("./skill-DW68KWTY.mjs");
115
+ const { skillInstalled } = await import("./skill-CqiSkac0.mjs");
116
116
  const { homedir } = await import("node:os");
117
117
  const refreshGlobal = skillInstalled(homedir());
118
118
  const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -26,10 +26,11 @@
26
26
  "devDependencies": {
27
27
  "@types/node": "^26.1.1",
28
28
  "typescript": "^7.0.2",
29
- "vite-plus": "0.2.5"
29
+ "vite-plus": "0.2.5",
30
+ "@mediamonks/canopy-pages": "0.0.0"
30
31
  },
31
32
  "scripts": {
32
- "build": "vp pack && node ./scripts/sync-templates.mjs",
33
+ "build": "pnpm --filter @mediamonks/canopy-pages run build && vp pack && node ./scripts/sync-templates.mjs",
33
34
  "dev": "vp pack --watch",
34
35
  "test": "vp test",
35
36
  "check": "vp check"
@@ -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--atbZ-TM.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 };