showpane 0.4.25 → 0.4.26

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.
Files changed (29) hide show
  1. package/bundle/meta/scaffold-manifest.json +6 -5
  2. package/bundle/scaffold/prisma/schema.local.prisma +1 -0
  3. package/bundle/scaffold/src/__tests__/branding.test.ts +43 -0
  4. package/bundle/scaffold/src/app/(portal)/client/page.tsx +42 -15
  5. package/bundle/scaffold/src/lib/branding.ts +47 -0
  6. package/bundle/scaffold/src/lib/portal-contracts.ts +1 -0
  7. package/bundle/toolchain/CLI_VERSION +1 -1
  8. package/bundle/toolchain/bin/create-portal.ts +50 -11
  9. package/bundle/toolchain/bin/export-runtime-state.ts +1 -0
  10. package/bundle/toolchain/skills/portal-analytics/SKILL.md +2 -2
  11. package/bundle/toolchain/skills/portal-create/SKILL.md +17 -11
  12. package/bundle/toolchain/skills/portal-create/SKILL.md.tmpl +15 -9
  13. package/bundle/toolchain/skills/portal-credentials/SKILL.md +2 -2
  14. package/bundle/toolchain/skills/portal-delete/SKILL.md +2 -2
  15. package/bundle/toolchain/skills/portal-deploy/SKILL.md +6 -3
  16. package/bundle/toolchain/skills/portal-deploy/SKILL.md.tmpl +4 -1
  17. package/bundle/toolchain/skills/portal-dev/SKILL.md +2 -2
  18. package/bundle/toolchain/skills/portal-list/SKILL.md +2 -2
  19. package/bundle/toolchain/skills/portal-onboard/SKILL.md +4 -2
  20. package/bundle/toolchain/skills/portal-onboard/SKILL.md.tmpl +2 -0
  21. package/bundle/toolchain/skills/portal-preview/SKILL.md +2 -2
  22. package/bundle/toolchain/skills/portal-setup/SKILL.md +2 -2
  23. package/bundle/toolchain/skills/portal-share/SKILL.md +2 -2
  24. package/bundle/toolchain/skills/portal-status/SKILL.md +2 -2
  25. package/bundle/toolchain/skills/portal-update/SKILL.md +2 -2
  26. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +2 -2
  27. package/bundle/toolchain/skills/portal-verify/SKILL.md +2 -2
  28. package/dist/index.js +6 -2
  29. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-04-13T22:00:52.340Z",
3
+ "generatedAt": "2026-04-13T22:42:22.971Z",
4
4
  "scaffoldVersion": "0.2.7",
5
5
  "files": {
6
6
  ".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
@@ -11,7 +11,7 @@
11
11
  "package.json": "b095e17e7fc181c630e87fe9f473c5a4ef969afcd4b110f9f9c6d6a6d93f1c0b",
12
12
  "postcss.config.js": "fa650b380adfabb151a0b352f7135e107e6352345f899060f1c5c231228f94bf",
13
13
  "prisma.config.ts": "36f56fd74eae70632e484443e38d08665158d72d5c978dc456651d8d5e1a636e",
14
- "prisma/schema.local.prisma": "f5d6f3cb17d6d229f46ef82eef7c0ff4261596924f0173fef075ac394f423073",
14
+ "prisma/schema.local.prisma": "90b9bc60e31a968137a574f156ad83ed5e46a45a8d6080b40126118806b9cd34",
15
15
  "prisma/seed.ts": "01b4295296676ab415ab2b30b4e39e13c3fdd30c3bab2b22a7cca672d1c2ea90",
16
16
  "public/example-avatar.svg": "0edeb0d3fbefa89cc27ffe6564d20e3ee0fd073cb6d9f2a025248ef3b3f277fd",
17
17
  "public/example-logo.svg": "bc5cd933aff2a17698dee66a7b4ea940ad12238e9d813474d643b459b1e8d6da",
@@ -19,6 +19,7 @@
19
19
  "scripts/prisma-db-push.mjs": "76ac85fe65b5dc3d9cc7432e44618fcc84b7443574c8d88198d01f13ac23c040",
20
20
  "scripts/prisma-generate.mjs": "d371e63388fa39f963b7c3c7cb8f87e0d9cd43cbf69d254b999108e29b8738c8",
21
21
  "scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
22
+ "src/__tests__/branding.test.ts": "dffbb3744705bc378ad91b9d7b8868004ffeaa45623f4748e1fed951096b9e3f",
22
23
  "src/__tests__/client-portals.test.ts": "9c3236bf0f7190b7d5ba9082287dcb29bc00d28dd63782a89505125ead06c624",
23
24
  "src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
24
25
  "src/__tests__/portal-contracts.test.ts": "a3ecf29c460a14f22ef48449fc9de5b945ce5f03423f12380d05e01159d1e3b4",
@@ -27,7 +28,7 @@
27
28
  "src/app/(portal)/client/example/example-client.tsx": "ed32b111acea861f448d865338f8841d47c6ca7c2f87ed30d85bb0804940d4ec",
28
29
  "src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
29
30
  "src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
30
- "src/app/(portal)/client/page.tsx": "7efe97c047e9ad8f5d9034fe616c0c915277a9cb92fa4ce57fe35af672c80320",
31
+ "src/app/(portal)/client/page.tsx": "cf0d1b0517aadf1bfbfc91552d0c13201a140c31d2df9e08e1c62a5d48e59189",
31
32
  "src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
32
33
  "src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
33
34
  "src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
@@ -43,7 +44,7 @@
43
44
  "src/components/portal-login.tsx": "8b0d91bb28674e1102fd2e5b5ddcc3a93755dd806fbd3d1b2dbea2646cffca5e",
44
45
  "src/components/portal-shell.tsx": "f46a0f753a4a0318f06c8b4e46295febb84b03ea082c95057a6da50c737b4e21",
45
46
  "src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
46
- "src/lib/branding.ts": "cc55f40e02bc3e486b227988f95739ca1cda8012c97b591295995eb4465efd57",
47
+ "src/lib/branding.ts": "f0fa242c285610105c27fcaf76828f1892c8051fc2db5c36bdbe92ea916aa4bd",
47
48
  "src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
48
49
  "src/lib/client-portals.ts": "9b531f9a9ea459b4ab85257b9dd282874fa1422838fe89d511940e417114216a",
49
50
  "src/lib/control-plane.ts": "e0cf39f28ec7de715fd5cfbb5f4240773fcd3d775cd1677588dd749fff740a0e",
@@ -51,7 +52,7 @@
51
52
  "src/lib/deploy-bundle.ts": "e9675cccb2c802e408639481986c6b629737541853e1c93f322c08a5b9dfc5f9",
52
53
  "src/lib/files.ts": "24fd8d1d53c180d62441019395fb140ba3baa28311918ac488284adcdda8eb9a",
53
54
  "src/lib/load-app-env.ts": "78b80e17d896885f0d72315ee9a6cf7a0a8c6c08171f26e3d599bb9b2e8afeee",
54
- "src/lib/portal-contracts.ts": "e3521b117239d0b0f9bc86a08607c6bb26f93dc1c47cd2dcf0485c2ca7358835",
55
+ "src/lib/portal-contracts.ts": "927695c64d4f7b56be9e7c0bc37fac105d37558404df8eac09c6f0d1a8f27669",
55
56
  "src/lib/prisma-client.ts": "28cd100129a0178a6c8fdfe49e6997b19983fcc427b9fa7caee3ac26226e5eb3",
56
57
  "src/lib/runtime-state.ts": "3d30de7dfeaaa48d8b6fd5d29976ecd001408172100c95b063d5d804fdce0a2e",
57
58
  "src/lib/storage.ts": "ae3b85fc6cccd39d4174a391dcbe6e91fb9460eb407ec9dbfedd63594a441d08",
@@ -69,6 +69,7 @@ model ClientPortal {
69
69
  organizationId String
70
70
  slug String
71
71
  companyName String
72
+ websiteUrl String?
72
73
  logoUrl String?
73
74
 
74
75
  // Auth
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ getBrandLogoUrl,
5
+ getDomainFromWebsite,
6
+ getLogoUrl,
7
+ normalizeWebsiteUrl,
8
+ } from "@/lib/branding";
9
+
10
+ describe("branding helpers", () => {
11
+ it("normalizes bare domains into https URLs", () => {
12
+ expect(normalizeWebsiteUrl("bidgen.io")).toBe("https://bidgen.io/");
13
+ expect(normalizeWebsiteUrl("https://bidgen.io")).toBe("https://bidgen.io/");
14
+ });
15
+
16
+ it("extracts hostnames from bare domains and full URLs", () => {
17
+ expect(getDomainFromWebsite("bidgen.io")).toBe("bidgen.io");
18
+ expect(getDomainFromWebsite("https://www.bidgen.io/path")).toBe("www.bidgen.io");
19
+ });
20
+
21
+ it("prefers a stored logo URL, then website-derived logos, then initials", () => {
22
+ expect(
23
+ getBrandLogoUrl({
24
+ logoUrl: "https://cdn.example/logo.png",
25
+ websiteUrl: "bidgen.io",
26
+ fallbackName: "Bidgen",
27
+ }),
28
+ ).toBe("https://cdn.example/logo.png");
29
+
30
+ expect(
31
+ getBrandLogoUrl({
32
+ websiteUrl: "bidgen.io",
33
+ fallbackName: "Bidgen",
34
+ }),
35
+ ).toBe(getLogoUrl("bidgen.io", "Bidgen"));
36
+
37
+ expect(
38
+ getBrandLogoUrl({
39
+ fallbackName: "Bidgen",
40
+ }),
41
+ ).toBe(getLogoUrl("", "Bidgen"));
42
+ });
43
+ });
@@ -1,5 +1,5 @@
1
1
  import { PortalLogin } from "@/components/portal-login";
2
- import { getInitialLogo } from "@/lib/branding";
2
+ import { getBrandLogoUrl } from "@/lib/branding";
3
3
  import { prisma } from "@/lib/db";
4
4
  import { resolveDefaultOrganizationId } from "@/lib/client-portals";
5
5
  import { getRuntimePortalBySlug, getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
@@ -12,12 +12,13 @@ export default async function ClientLogin({
12
12
  const params = (await searchParams) ?? {};
13
13
  const portalSlug = params.portal?.trim() || null;
14
14
 
15
- let companyName = "Your Portal";
15
+ let companyName = "Showpane";
16
16
  let companyUrl = "https://showpane.com";
17
17
  let supportEmail = "support@showpane.com";
18
18
  let portalLabel = "Client Portal";
19
19
  let description = "Private portal access. Sign in with the credentials you were sent.";
20
- let companyInitial = "P";
20
+ let companyLogoSrc: string | null = null;
21
+ let companyLogoAlt = companyName;
21
22
 
22
23
  if (portalSlug) {
23
24
  try {
@@ -25,10 +26,16 @@ export default async function ClientLogin({
25
26
  const state = await getRuntimeState();
26
27
  const portal = await getRuntimePortalBySlug(portalSlug);
27
28
  if (portal) {
28
- companyName = portal.companyName;
29
- portalLabel = `${portal.companyName} Portal`;
30
- description = `Private portal for ${portal.companyName}. Sign in with the credentials you were sent.`;
31
- companyInitial = portal.companyName[0]?.toUpperCase() || "P";
29
+ const orgName = state?.organization?.name || companyName;
30
+ companyName = orgName;
31
+ companyLogoAlt = orgName;
32
+ portalLabel = state?.organization?.portalLabel || `${orgName} Portal`;
33
+ description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
34
+ companyLogoSrc = getBrandLogoUrl({
35
+ logoUrl: state?.organization?.logoUrl,
36
+ websiteUrl: state?.organization?.websiteUrl,
37
+ fallbackName: orgName,
38
+ });
32
39
  if (state?.organization?.websiteUrl) {
33
40
  companyUrl = state.organization.websiteUrl;
34
41
  }
@@ -45,11 +52,15 @@ export default async function ClientLogin({
45
52
  slug: portalSlug,
46
53
  isActive: true,
47
54
  },
48
- select: { companyName: true },
55
+ select: {
56
+ companyName: true,
57
+ },
49
58
  });
50
59
  const organization = await prisma.organization.findUnique({
51
60
  where: { id: organizationId },
52
61
  select: {
62
+ name: true,
63
+ logoUrl: true,
53
64
  websiteUrl: true,
54
65
  supportEmail: true,
55
66
  portalLabel: true,
@@ -57,10 +68,16 @@ export default async function ClientLogin({
57
68
  });
58
69
 
59
70
  if (portal) {
60
- companyName = portal.companyName;
61
- portalLabel = organization?.portalLabel || `${portal.companyName} Portal`;
62
- description = `Private portal for ${portal.companyName}. Sign in with the credentials you were sent.`;
63
- companyInitial = portal.companyName[0]?.toUpperCase() || "P";
71
+ const orgName = organization?.name || companyName;
72
+ companyName = orgName;
73
+ companyLogoAlt = orgName;
74
+ portalLabel = organization?.portalLabel || `${orgName} Portal`;
75
+ description = `Private portal created by ${orgName} for ${portal.companyName}. Sign in with the credentials you were sent.`;
76
+ companyLogoSrc = getBrandLogoUrl({
77
+ logoUrl: organization?.logoUrl,
78
+ websiteUrl: organization?.websiteUrl,
79
+ fallbackName: orgName,
80
+ });
64
81
  }
65
82
  if (organization?.websiteUrl) {
66
83
  companyUrl = organization.websiteUrl;
@@ -79,9 +96,19 @@ export default async function ClientLogin({
79
96
  <PortalLogin
80
97
  companyName={companyName}
81
98
  companyLogo={
82
- <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
83
- <span className="text-xs font-bold text-white">{companyInitial}</span>
84
- </div>
99
+ companyLogoSrc ? (
100
+ <img
101
+ src={companyLogoSrc}
102
+ alt={companyLogoAlt}
103
+ className="h-7 w-7 rounded-lg object-cover"
104
+ />
105
+ ) : (
106
+ <div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
107
+ <span className="text-xs font-bold text-white">
108
+ {companyName[0]?.toUpperCase() || "S"}
109
+ </span>
110
+ </div>
111
+ )
85
112
  }
86
113
  companyUrl={companyUrl}
87
114
  supportEmail={supportEmail}
@@ -4,6 +4,36 @@
4
4
  * All functions return sensible defaults on failure.
5
5
  */
6
6
 
7
+ /**
8
+ * Normalize a website or bare domain into an https URL.
9
+ */
10
+ export function normalizeWebsiteUrl(value?: string | null): string | null {
11
+ if (!value) return null;
12
+ const trimmed = value.trim();
13
+ if (!trimmed) return null;
14
+
15
+ const candidate = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
16
+ try {
17
+ return new URL(candidate).toString();
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Extract a hostname from a website URL or bare domain.
25
+ */
26
+ export function getDomainFromWebsite(value?: string | null): string | null {
27
+ const normalized = normalizeWebsiteUrl(value);
28
+ if (!normalized) return null;
29
+
30
+ try {
31
+ return new URL(normalized).hostname;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
7
37
  /**
8
38
  * Fetch a company logo URL from a domain.
9
39
  * Uses Clearbit Logo API (free, no key required).
@@ -19,6 +49,23 @@ export function getLogoUrl(domain: string, fallbackName?: string): string {
19
49
  return `https://ui-avatars.com/api/?name=${initial}&background=111827&color=fff&size=128&bold=true`;
20
50
  }
21
51
 
52
+ /**
53
+ * Resolve the best available logo source for a brand.
54
+ * Prefers a stored logo URL, then a website-derived domain, then initials.
55
+ */
56
+ export function getBrandLogoUrl(options: {
57
+ logoUrl?: string | null;
58
+ websiteUrl?: string | null;
59
+ fallbackName: string;
60
+ }): string {
61
+ if (options.logoUrl) {
62
+ return options.logoUrl;
63
+ }
64
+
65
+ const domain = getDomainFromWebsite(options.websiteUrl);
66
+ return getLogoUrl(domain ?? "", options.fallbackName);
67
+ }
68
+
22
69
  /**
23
70
  * Fetch a Gravatar URL for an email address.
24
71
  * Falls back to UI Avatars if no Gravatar exists.
@@ -87,6 +87,7 @@ export interface PortalFileSyncManifestPayload {
87
87
  export interface RuntimePortalSnapshot {
88
88
  slug: string;
89
89
  companyName: string;
90
+ websiteUrl?: string | null;
90
91
  logoUrl?: string | null;
91
92
  username: string;
92
93
  passwordHash: string;
@@ -1 +1 @@
1
- 0.4.25
1
+ 0.4.26
@@ -1,4 +1,5 @@
1
1
  import { PrismaClient } from "@/lib/prisma-client";
2
+ import { getBrandLogoUrl, normalizeWebsiteUrl } from "@/lib/branding";
2
3
  import bcrypt from "bcryptjs";
3
4
  import crypto from "node:crypto";
4
5
 
@@ -16,7 +17,7 @@ async function main() {
16
17
  const args = process.argv.slice(2);
17
18
 
18
19
  if (args.includes("--help")) {
19
- console.log("Usage: create-portal --slug <slug> --company <name> --org-id <orgId>");
20
+ console.log("Usage: create-portal --slug <slug> --company <name> --org-id <orgId> [--website <domain-or-url>]");
20
21
  console.log("Creates a new client portal with auto-generated credentials.");
21
22
  process.exit(0);
22
23
  }
@@ -25,6 +26,7 @@ async function main() {
25
26
  const slug = getArg("--slug");
26
27
  const company = getArg("--company");
27
28
  const orgId = getArg("--org-id");
29
+ const website = getArg("--website");
28
30
 
29
31
  if (!slug || !company || !orgId) fail("Missing --slug, --company, or --org-id");
30
32
 
@@ -37,6 +39,11 @@ async function main() {
37
39
  const username = slug;
38
40
  const password = crypto.randomBytes(16).toString("base64url");
39
41
  const passwordHash = await bcrypt.hash(password, 10);
42
+ const websiteUrl = normalizeWebsiteUrl(website);
43
+ const logoUrl = getBrandLogoUrl({
44
+ websiteUrl,
45
+ fallbackName: company,
46
+ });
40
47
 
41
48
  const prisma = new PrismaClient();
42
49
  try {
@@ -46,19 +53,51 @@ async function main() {
46
53
  });
47
54
  if (existing) fail(`Slug "${slug}" already exists`);
48
55
 
49
- const portal = await prisma.clientPortal.create({
50
- data: {
51
- organizationId: orgId,
52
- slug,
53
- companyName: company,
54
- username,
55
- passwordHash,
56
- },
57
- });
56
+ const createData: Record<string, unknown> = {
57
+ organizationId: orgId,
58
+ slug,
59
+ companyName: company,
60
+ logoUrl,
61
+ username,
62
+ passwordHash,
63
+ };
64
+
65
+ // Older scaffolded apps may not have the ClientPortal.websiteUrl column yet.
66
+ if (websiteUrl) {
67
+ createData.websiteUrl = websiteUrl;
68
+ }
69
+
70
+ let portal;
71
+ try {
72
+ portal = await prisma.clientPortal.create({
73
+ data: createData,
74
+ });
75
+ } catch (error) {
76
+ const message = error instanceof Error ? error.message : String(error);
77
+ if (
78
+ websiteUrl &&
79
+ (message.includes("Unknown arg `websiteUrl`") ||
80
+ message.includes("Unknown argument `websiteUrl`"))
81
+ ) {
82
+ delete createData.websiteUrl;
83
+ portal = await prisma.clientPortal.create({
84
+ data: createData,
85
+ });
86
+ } else {
87
+ throw error;
88
+ }
89
+ }
58
90
 
59
91
  console.log(JSON.stringify({
60
92
  ok: true,
61
- portal: { id: portal.id, slug: portal.slug, username, password },
93
+ portal: {
94
+ id: portal.id,
95
+ slug: portal.slug,
96
+ username,
97
+ password,
98
+ websiteUrl: portal.websiteUrl,
99
+ logoUrl: portal.logoUrl,
100
+ },
62
101
  }));
63
102
  } finally {
64
103
  await prisma.$disconnect();
@@ -51,6 +51,7 @@ async function main() {
51
51
  select: {
52
52
  slug: true,
53
53
  companyName: true,
54
+ websiteUrl: true,
54
55
  logoUrl: true,
55
56
  username: true,
56
57
  passwordHash: true,
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -104,9 +104,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
104
104
 
105
105
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
106
106
 
107
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
107
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
108
108
 
109
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
109
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
110
110
 
111
111
  ## Steps
112
112
 
@@ -168,9 +168,15 @@ The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":
168
168
 
169
169
  If invalid, explain the issue and ask for a different slug.
170
170
 
171
- Also ask for the client's website domain (e.g., "acme-health.com"). This is optional but enables auto-branding:
172
- - If provided, the client logo will be fetched via `getLogoUrl(domain)` and stored in `ClientPortal.logoUrl`
173
- - If not provided, an initial-based logo is generated via `getInitialLogo(companyName)` and stored as a data URI
171
+ Also determine the client's website domain. This enables auto-branding and is worth a quick best-effort pass:
172
+ - if the domain is explicit in the transcript or user prompt, use it
173
+ - otherwise make one quick best-effort lookup for the official site
174
+ - if one clear candidate stands out, suggest it briefly: `I found <domain> — I'll use that for the logo unless you want a different one`
175
+ - if confidence is low, ask directly
176
+
177
+ Store the confirmed value in `ClientPortal.websiteUrl`.
178
+ Use it to derive `ClientPortal.logoUrl` via `getBrandLogoUrl(...)`.
179
+ If no domain is confirmed, fall back to an initial-based logo.
174
180
 
175
181
  ### Step 3: Granola MCP integration (optional)
176
182
 
@@ -284,10 +290,10 @@ import { PortalShell } from "@/components/portal-shell";
284
290
  - The exported component returns `<PortalShell>` with all required props
285
291
 
286
292
  **PortalShell props (all required):**
287
- - `companyName` — the org's company name (from `ORG_NAME`)
288
- - `companyLogo` — a `<span>` with the first letter of the company name, white text
293
+ - `companyName` — the org's company name (from `get-org.ts`)
294
+ - `companyLogo` — prefer the org logo via `getBrandLogoUrl({ logoUrl: ORG.logoUrl, websiteUrl: ORG.websiteUrl, fallbackName: ORG.name })`; fall back to initials only if no better source exists
289
295
  - `clientName` — the client's company name (from transcript or user input)
290
- - `clientLogoSrc` — if client domain was provided: use `getLogoUrl(domain)` from `app/src/lib/branding.ts`. If not: use `getInitialLogo(clientName)` to generate an SVG data URI. Store the chosen URL in the ClientPortal record's `logoUrl` field
296
+ - `clientLogoSrc` — use `getBrandLogoUrl({ websiteUrl: <confirmed client website>, fallbackName: clientName })`; store the chosen URL in the `ClientPortal.logoUrl` field
291
297
  - `clientLogoAlt` — the client company name
292
298
  - `lastUpdated` — today's date formatted as "7 April 2026"
293
299
  - `contact` — object with `name`, `title`, `avatarSrc`, `email` (from the `get-org.ts` result, not from ad-hoc config or DB probing)
@@ -335,12 +341,12 @@ Import only the icons you need from `lucide-react`. Common choices:
335
341
  Run the create-portal script to register the portal in the database:
336
342
 
337
343
  ```bash
338
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID"
344
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID" --website "<confirmed_client_website_if_any>"
339
345
  ```
340
346
 
341
347
  This creates the `ClientPortal` record with the slug and company name, links it to
342
- the Organization, and currently auto-generates initial credentials. The script
343
- returns `username` and `password`.
348
+ the Organization, stores the confirmed client website/logo if available, and
349
+ currently auto-generates initial credentials. The script returns `username` and `password`.
344
350
 
345
351
  Do not dump those credentials into the middle of the flow unless the user asked.
346
352
  For onboarding, carry them forward quietly and show them at the access phase.
@@ -73,9 +73,15 @@ The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":
73
73
 
74
74
  If invalid, explain the issue and ask for a different slug.
75
75
 
76
- Also ask for the client's website domain (e.g., "acme-health.com"). This is optional but enables auto-branding:
77
- - If provided, the client logo will be fetched via `getLogoUrl(domain)` and stored in `ClientPortal.logoUrl`
78
- - If not provided, an initial-based logo is generated via `getInitialLogo(companyName)` and stored as a data URI
76
+ Also determine the client's website domain. This enables auto-branding and is worth a quick best-effort pass:
77
+ - if the domain is explicit in the transcript or user prompt, use it
78
+ - otherwise make one quick best-effort lookup for the official site
79
+ - if one clear candidate stands out, suggest it briefly: `I found <domain> — I'll use that for the logo unless you want a different one`
80
+ - if confidence is low, ask directly
81
+
82
+ Store the confirmed value in `ClientPortal.websiteUrl`.
83
+ Use it to derive `ClientPortal.logoUrl` via `getBrandLogoUrl(...)`.
84
+ If no domain is confirmed, fall back to an initial-based logo.
79
85
 
80
86
  ### Step 3: Granola MCP integration (optional)
81
87
 
@@ -189,10 +195,10 @@ import { PortalShell } from "@/components/portal-shell";
189
195
  - The exported component returns `<PortalShell>` with all required props
190
196
 
191
197
  **PortalShell props (all required):**
192
- - `companyName` — the org's company name (from `ORG_NAME`)
193
- - `companyLogo` — a `<span>` with the first letter of the company name, white text
198
+ - `companyName` — the org's company name (from `get-org.ts`)
199
+ - `companyLogo` — prefer the org logo via `getBrandLogoUrl({ logoUrl: ORG.logoUrl, websiteUrl: ORG.websiteUrl, fallbackName: ORG.name })`; fall back to initials only if no better source exists
194
200
  - `clientName` — the client's company name (from transcript or user input)
195
- - `clientLogoSrc` — if client domain was provided: use `getLogoUrl(domain)` from `app/src/lib/branding.ts`. If not: use `getInitialLogo(clientName)` to generate an SVG data URI. Store the chosen URL in the ClientPortal record's `logoUrl` field
201
+ - `clientLogoSrc` — use `getBrandLogoUrl({ websiteUrl: <confirmed client website>, fallbackName: clientName })`; store the chosen URL in the `ClientPortal.logoUrl` field
196
202
  - `clientLogoAlt` — the client company name
197
203
  - `lastUpdated` — today's date formatted as "7 April 2026"
198
204
  - `contact` — object with `name`, `title`, `avatarSrc`, `email` (from the `get-org.ts` result, not from ad-hoc config or DB probing)
@@ -240,12 +246,12 @@ Import only the icons you need from `lucide-react`. Common choices:
240
246
  Run the create-portal script to register the portal in the database:
241
247
 
242
248
  ```bash
243
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID"
249
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID" --website "<confirmed_client_website_if_any>"
244
250
  ```
245
251
 
246
252
  This creates the `ClientPortal` record with the slug and company name, links it to
247
- the Organization, and currently auto-generates initial credentials. The script
248
- returns `username` and `password`.
253
+ the Organization, stores the confirmed client website/logo if available, and
254
+ currently auto-generates initial credentials. The script returns `username` and `password`.
249
255
 
250
256
  Do not dump those credentials into the middle of the flow unless the user asked.
251
257
  For onboarding, carry them forward quietly and show them at the access phase.
@@ -109,9 +109,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
109
109
 
110
110
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
111
111
 
112
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
112
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
113
113
 
114
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
114
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
115
115
 
116
116
  ## Steps
117
117
 
@@ -109,9 +109,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
109
109
 
110
110
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
111
111
 
112
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
112
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
113
113
 
114
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
114
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
115
115
 
116
116
  ## Safety Guard
117
117
 
@@ -102,9 +102,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
102
102
 
103
103
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
104
104
 
105
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
105
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
106
106
 
107
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
107
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
108
108
 
109
109
  ## Steps
110
110
 
@@ -168,12 +168,15 @@ Run list-portals and warn about portals missing credentials. This is a warning,
168
168
  ### Cloud Step 2: Run the canonical deploy command
169
169
 
170
170
  Use the built-in deploy command instead of reimplementing the staged cloud protocol in shell.
171
+ Run it directly so progress lines stay visible while the build and publish are in flight.
171
172
 
172
173
  ```bash
173
- DEPLOY_JSON=$(cd "$APP_PATH" && SHOWPANE_APP_PATH="$APP_PATH" NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$SKILL_DIR/bin/tsconfig.json" "$SKILL_DIR/bin/deploy-to-cloud.ts" --app-path "$APP_PATH" --wait --json)
174
+ DEPLOY_JSON=$(cd "$APP_PATH" && SHOWPANE_APP_PATH="$APP_PATH" "$SHOWPANE_BIN/showpane" deploy --wait --json)
174
175
  echo "$DEPLOY_JSON"
175
176
  ```
176
177
 
178
+ Do not wrap this command in `tail` or another truncating pipe. If you need a shorter summary later, capture the output after the command finishes or read from a temp log separately.
179
+
177
180
  That command already owns:
178
181
  - type check
179
182
  - project-link bootstrap
@@ -73,12 +73,15 @@ Run list-portals and warn about portals missing credentials. This is a warning,
73
73
  ### Cloud Step 2: Run the canonical deploy command
74
74
 
75
75
  Use the built-in deploy command instead of reimplementing the staged cloud protocol in shell.
76
+ Run it directly so progress lines stay visible while the build and publish are in flight.
76
77
 
77
78
  ```bash
78
- DEPLOY_JSON=$(cd "$APP_PATH" && SHOWPANE_APP_PATH="$APP_PATH" NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$SKILL_DIR/bin/tsconfig.json" "$SKILL_DIR/bin/deploy-to-cloud.ts" --app-path "$APP_PATH" --wait --json)
79
+ DEPLOY_JSON=$(cd "$APP_PATH" && SHOWPANE_APP_PATH="$APP_PATH" "$SHOWPANE_BIN/showpane" deploy --wait --json)
79
80
  echo "$DEPLOY_JSON"
80
81
  ```
81
82
 
83
+ Do not wrap this command in `tail` or another truncating pipe. If you need a shorter summary later, capture the output after the command finishes or read from a temp log separately.
84
+
82
85
  That command already owns:
83
86
  - type check
84
87
  - project-link bootstrap
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Steps
107
107
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -278,6 +278,7 @@ During this phase:
278
278
 
279
279
  - suggest a slug if needed
280
280
  - prefer a real company/client name in the portal
281
+ - if the client website is not already obvious, do one quick best-effort domain lookup and suggest the result briefly before drafting
281
282
  - prefer useful structure over completeness
282
283
  - aim for a credible first draft, not a perfect final artifact
283
284
  - do not re-ask for the company name or template if the user already chose them
@@ -380,6 +381,7 @@ Save checkpoint with phase `access-ready` and `accessMode`.
380
381
  ### Phase 8: Publish to Showpane Cloud
381
382
 
382
383
  Run the `portal-deploy` flow inline.
384
+ Let the deploy command stream its own progress. Do not wrap the long-running publish step in `tail`.
383
385
 
384
386
  This phase is part of onboarding by default. Do not treat publish as an optional
385
387
  afterthought unless the user explicitly says they want to stop at local preview.
@@ -185,6 +185,7 @@ During this phase:
185
185
 
186
186
  - suggest a slug if needed
187
187
  - prefer a real company/client name in the portal
188
+ - if the client website is not already obvious, do one quick best-effort domain lookup and suggest the result briefly before drafting
188
189
  - prefer useful structure over completeness
189
190
  - aim for a credible first draft, not a perfect final artifact
190
191
  - do not re-ask for the company name or template if the user already chose them
@@ -287,6 +288,7 @@ Save checkpoint with phase `access-ready` and `accessMode`.
287
288
  ### Phase 8: Publish to Showpane Cloud
288
289
 
289
290
  Run the `portal-deploy` flow inline.
291
+ Let the deploy command stream its own progress. Do not wrap the long-running publish step in `tail`.
290
292
 
291
293
  This phase is part of onboarding by default. Do not treat publish as an optional
292
294
  afterthought unless the user explicitly says they want to stop at local preview.
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -93,9 +93,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
93
93
 
94
94
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
95
95
 
96
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
96
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
97
97
 
98
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
98
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
99
99
 
100
100
  ## Steps
101
101
 
@@ -102,9 +102,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
102
102
 
103
103
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
104
104
 
105
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
105
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
106
106
 
107
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
107
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
108
108
 
109
109
  ## Overview
110
110
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Steps
107
107
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Overview
107
107
 
@@ -99,9 +99,9 @@ If `RECENT_SKILLS` is shown, suggest the likely next skill:
99
99
 
100
100
  If `RECENT_LEARNINGS` is shown, review them before proceeding. Apply them where relevant but do not mention them unless they materially affect the current task.
101
101
 
102
- Read `skills/shared/runtime-principles.md` once near the start of the skill and apply the relevant product defaults.
102
+ Read `skills/shared/runtime-principles.md` directly from that exact path near the start of the skill and apply the relevant product defaults.
103
103
 
104
- If `skills/shared/platform-constraints.md` exists, read it once near the start of the skill and apply only the relevant limits.
104
+ If `skills/shared/platform-constraints.md` exists, read it directly from that exact path near the start of the skill and apply only the relevant limits. No directory listing is needed first.
105
105
 
106
106
  ## Steps
107
107
 
package/dist/index.js CHANGED
@@ -671,8 +671,12 @@ async function syncCloudProjectLink(projectRoot, accessToken) {
671
671
  writeCloudProjectLink(projectRoot, projectLink);
672
672
  }
673
673
  function removePath(targetPath) {
674
- if (!existsSync(targetPath)) return;
675
- const stat = lstatSync(targetPath);
674
+ let stat;
675
+ try {
676
+ stat = lstatSync(targetPath);
677
+ } catch {
678
+ return;
679
+ }
676
680
  if (stat.isSymbolicLink() || stat.isFile()) {
677
681
  unlinkSync(targetPath);
678
682
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.25",
3
+ "version": "0.4.26",
4
4
  "description": "CLI for Showpane — AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {