showpane 0.4.1 → 0.4.3

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 (106) hide show
  1. package/README.md +26 -1
  2. package/bundle/meta/scaffold-manifest.json +73 -0
  3. package/bundle/scaffold/VERSION +1 -0
  4. package/bundle/scaffold/__dot__env.example +24 -0
  5. package/bundle/scaffold/__dot__gitignore +41 -0
  6. package/bundle/scaffold/docker/Caddyfile +3 -0
  7. package/bundle/scaffold/docker/Dockerfile +30 -0
  8. package/bundle/scaffold/docker-compose.yml +53 -0
  9. package/bundle/scaffold/next.config.ts +20 -0
  10. package/bundle/scaffold/package-lock.json +5843 -0
  11. package/bundle/scaffold/package.json +42 -0
  12. package/bundle/scaffold/postcss.config.js +6 -0
  13. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
  14. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
  15. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
  16. package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
  17. package/bundle/scaffold/prisma/schema.local.prisma +131 -0
  18. package/bundle/scaffold/prisma/schema.prisma +128 -0
  19. package/bundle/scaffold/prisma/seed.ts +49 -0
  20. package/bundle/scaffold/public/example-avatar.svg +4 -0
  21. package/bundle/scaffold/public/example-logo.svg +4 -0
  22. package/bundle/scaffold/public/robots.txt +2 -0
  23. package/bundle/scaffold/scripts/backup.sh +19 -0
  24. package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
  25. package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
  26. package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
  27. package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
  28. package/bundle/scaffold/scripts/restore.sh +31 -0
  29. package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
  30. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
  31. package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
  32. package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
  33. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
  34. package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
  35. package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
  36. package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
  37. package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
  38. package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
  39. package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
  40. package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
  41. package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
  42. package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
  43. package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
  44. package/bundle/scaffold/src/app/api/health/route.ts +19 -0
  45. package/bundle/scaffold/src/app/globals.css +7 -0
  46. package/bundle/scaffold/src/app/layout.tsx +25 -0
  47. package/bundle/scaffold/src/app/page.tsx +177 -0
  48. package/bundle/scaffold/src/components/portal-login.tsx +169 -0
  49. package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
  50. package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
  51. package/bundle/scaffold/src/lib/branding.ts +50 -0
  52. package/bundle/scaffold/src/lib/client-auth.ts +98 -0
  53. package/bundle/scaffold/src/lib/client-portals.ts +134 -0
  54. package/bundle/scaffold/src/lib/control-plane.ts +100 -0
  55. package/bundle/scaffold/src/lib/db.ts +7 -0
  56. package/bundle/scaffold/src/lib/files.ts +124 -0
  57. package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
  58. package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
  59. package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
  60. package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
  61. package/bundle/scaffold/src/lib/storage.ts +204 -0
  62. package/bundle/scaffold/src/lib/token.ts +186 -0
  63. package/bundle/scaffold/src/lib/utils.ts +6 -0
  64. package/bundle/scaffold/src/middleware.ts +61 -0
  65. package/bundle/scaffold/tailwind.config.ts +15 -0
  66. package/bundle/scaffold/tests/__dot__gitkeep +0 -0
  67. package/bundle/scaffold/tsconfig.json +23 -0
  68. package/bundle/scaffold/vitest.config.ts +13 -0
  69. package/bundle/toolchain/VERSION +1 -0
  70. package/bundle/toolchain/bin/check-slug.ts +59 -0
  71. package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
  72. package/bundle/toolchain/bin/create-portal.ts +71 -0
  73. package/bundle/toolchain/bin/delete-portal.ts +48 -0
  74. package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
  75. package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
  76. package/bundle/toolchain/bin/generate-share-link.ts +68 -0
  77. package/bundle/toolchain/bin/list-portals.ts +53 -0
  78. package/bundle/toolchain/bin/materialize-file.ts +35 -0
  79. package/bundle/toolchain/bin/query-analytics.ts +88 -0
  80. package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
  81. package/bundle/toolchain/bin/showpane-config +63 -0
  82. package/bundle/toolchain/bin/tsconfig.json +13 -0
  83. package/bundle/toolchain/skills/VERSION +1 -0
  84. package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
  85. package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
  86. package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
  87. package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
  88. package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
  89. package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
  90. package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
  91. package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
  92. package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
  93. package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
  94. package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
  95. package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
  96. package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
  97. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
  98. package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
  99. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
  100. package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
  101. package/bundle/toolchain/skills/shared/preamble.md +137 -0
  102. package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
  103. package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
  104. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
  105. package/dist/index.js +1248 -164
  106. package/package.json +3 -2
@@ -0,0 +1,177 @@
1
+ import { prisma } from "@/lib/db";
2
+ import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
3
+ import Link from "next/link";
4
+ import { Presentation, Briefcase, UserPlus } from "lucide-react";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+
8
+ const templates = [
9
+ {
10
+ name: "Sales Follow-up",
11
+ description: "Meeting notes, next steps, documents",
12
+ icon: Presentation,
13
+ },
14
+ {
15
+ name: "Consulting",
16
+ description: "Project overview, deliverables, timeline",
17
+ icon: Briefcase,
18
+ },
19
+ {
20
+ name: "Onboarding",
21
+ description: "Welcome, setup steps, resources",
22
+ icon: UserPlus,
23
+ },
24
+ ];
25
+
26
+ export default async function Home() {
27
+ let portalCount = 0;
28
+ const showpaneBinDir = path.join(os.homedir(), ".showpane", "bin");
29
+ const resumeCommand = (process.env.PATH ?? "").split(path.delimiter).includes(showpaneBinDir)
30
+ ? "showpane claude"
31
+ : "npx showpane claude";
32
+ try {
33
+ if (isRuntimeSnapshotMode()) {
34
+ const state = await getRuntimeState();
35
+ portalCount = state?.portals.length ?? 0;
36
+ } else {
37
+ portalCount = await prisma.clientPortal.count();
38
+ }
39
+ } catch {
40
+ // DB not ready yet — show welcome page with 0 portals
41
+ }
42
+
43
+ return (
44
+ <main className="min-h-screen flex flex-col">
45
+ {/* Hero zone */}
46
+ <div className="bg-gradient-to-b from-[#2C5278] to-[#5A8BB5] px-4 py-16 md:py-24 text-center relative overflow-hidden">
47
+ <div
48
+ className="absolute inset-0 opacity-[0.07]"
49
+ style={{
50
+ backgroundImage: "radial-gradient(circle, white 1px, transparent 1px)",
51
+ backgroundSize: "24px 24px",
52
+ }}
53
+ />
54
+ <div className="relative">
55
+ <h1 className="sr-only">SHOWPANE</h1>
56
+ <div
57
+ role="img"
58
+ aria-label="SHOWPANE"
59
+ className="font-mono text-white text-[0.45rem] leading-[1.1] sm:text-[0.55rem] md:text-xs whitespace-pre select-none mx-auto w-fit"
60
+ >
61
+ {`███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ █████╗ ███╗ ██╗███████╗
62
+ ██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗██╔══██╗████╗ ██║██╔════╝
63
+ ███████╗███████║██║ ██║██║ █╗ ██║██████╔╝███████║██╔██╗ ██║█████╗
64
+ ╚════██║██╔══██║██║ ██║██║███╗██║██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝
65
+ ███████║██║ ██║╚██████╔╝╚███╔███╔╝██║ ██║ ██║██║ ╚████║███████╗
66
+ ╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝`}
67
+ </div>
68
+ <p className="mt-6 text-white/90 text-lg">
69
+ Create professional client portals with Claude Code.
70
+ </p>
71
+ </div>
72
+ </div>
73
+
74
+ {/* Action zone */}
75
+ <div className="flex-1 bg-[#FDFBF7] px-4 py-12 md:py-16">
76
+ <div className="max-w-lg mx-auto">
77
+ {/* Steps */}
78
+ <ol className="space-y-4">
79
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
80
+ <div className="flex items-start gap-4">
81
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
82
+ 1
83
+ </span>
84
+ <div className="min-w-0">
85
+ <p className="text-gray-900 font-medium mb-2">
86
+ In a terminal, reopen your Showpane workspace
87
+ </p>
88
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
89
+ {resumeCommand}
90
+ </code>
91
+ </div>
92
+ </div>
93
+ </li>
94
+
95
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
96
+ <div className="flex items-start gap-4">
97
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
98
+ 2
99
+ </span>
100
+ <div className="min-w-0">
101
+ <p className="text-gray-900 font-medium mb-2">
102
+ Use the fast path slash command
103
+ </p>
104
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded">
105
+ /portal create acme-health
106
+ </code>
107
+ </div>
108
+ </div>
109
+ </li>
110
+
111
+ <li className="border border-gray-200 rounded-lg p-5 bg-white">
112
+ <div className="flex items-start gap-4">
113
+ <span className="flex-shrink-0 w-7 h-7 rounded-full bg-gray-900 text-white text-sm font-medium flex items-center justify-center">
114
+ 3
115
+ </span>
116
+ <div className="min-w-0">
117
+ <p className="text-gray-900 font-medium mb-2">
118
+ Or tell it what to create
119
+ </p>
120
+ <code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
121
+ Create a portal for my call with [client name]
122
+ </code>
123
+ </div>
124
+ </div>
125
+ </li>
126
+ </ol>
127
+
128
+ <p className="mt-4 text-xs text-gray-400 text-center">
129
+ Don&apos;t have Claude Code?{" "}
130
+ <a
131
+ href="https://claude.ai/code"
132
+ className="text-blue-600 hover:underline"
133
+ target="_blank"
134
+ rel="noopener noreferrer"
135
+ >
136
+ Install it here
137
+ </a>
138
+ </p>
139
+
140
+ {/* Template previews */}
141
+ <div className="mt-12">
142
+ <p className="text-sm font-medium text-gray-500 text-center mb-4">
143
+ Claude Code generates portals from templates
144
+ </p>
145
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
146
+ {templates.map((t) => (
147
+ <div
148
+ key={t.name}
149
+ className="border border-gray-200 rounded-lg p-4 bg-white text-center"
150
+ >
151
+ <t.icon className="h-5 w-5 text-gray-400 mx-auto mb-2" />
152
+ <p className="text-sm font-medium text-gray-900">{t.name}</p>
153
+ <p className="text-xs text-gray-500 mt-1">{t.description}</p>
154
+ </div>
155
+ ))}
156
+ </div>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ {/* Footer zone */}
162
+ <footer className="bg-[#FDFBF7] px-4 pb-8 text-center space-y-2">
163
+ {portalCount > 0 && (
164
+ <p className="text-sm text-gray-500">
165
+ You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
166
+ <Link href="/client" className="text-blue-600 hover:underline">
167
+ Go to login
168
+ </Link>
169
+ </p>
170
+ )}
171
+ <p className="text-xs text-gray-400">
172
+ Powered by Claude Code
173
+ </p>
174
+ </footer>
175
+ </main>
176
+ );
177
+ }
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ import { useState, FormEvent, ReactNode } from "react";
4
+ import { ArrowRight, Eye, EyeOff } from "lucide-react";
5
+
6
+ export type PortalLoginProps = {
7
+ companyName: string;
8
+ companyLogo: ReactNode;
9
+ companyUrl: string;
10
+ portalLabel?: string;
11
+ description?: string;
12
+ supportEmail: string;
13
+ authEndpoint?: string;
14
+ redirectBasePath?: string;
15
+ };
16
+
17
+ export function PortalLogin({
18
+ companyName,
19
+ companyLogo,
20
+ companyUrl,
21
+ portalLabel,
22
+ description,
23
+ supportEmail,
24
+ authEndpoint,
25
+ redirectBasePath,
26
+ }: PortalLoginProps) {
27
+ const [username, setUsername] = useState("");
28
+ const [password, setPassword] = useState("");
29
+ const [error, setError] = useState<{ message: string; hint?: string } | null>(null);
30
+ const [loading, setLoading] = useState(false);
31
+ const [showPassword, setShowPassword] = useState(false);
32
+
33
+ const resolvedPortalLabel = portalLabel ?? "Client Portal";
34
+ const resolvedDescription = description ?? `Private portal for ${companyName} clients. Sign in with the credentials we sent you.`;
35
+ const resolvedAuthEndpoint = authEndpoint ?? "/api/client-auth";
36
+ const resolvedRedirectBasePath = redirectBasePath ?? "/client";
37
+
38
+ let displayDomain: string;
39
+ try {
40
+ displayDomain = new URL(companyUrl).hostname;
41
+ } catch {
42
+ displayDomain = companyUrl;
43
+ }
44
+
45
+ async function handleSubmit(e: FormEvent) {
46
+ e.preventDefault();
47
+ if (loading) return;
48
+ setLoading(true);
49
+ setError(null);
50
+
51
+ try {
52
+ const res = await fetch(resolvedAuthEndpoint, {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify({ username: username.trim(), password }),
56
+ });
57
+
58
+ if (res.ok) {
59
+ const { slug } = await res.json();
60
+ window.location.href = `${resolvedRedirectBasePath}/${slug}`;
61
+ return;
62
+ }
63
+
64
+ if (res.status === 429) {
65
+ setError({ message: "Too many attempts", hint: "Please wait a minute before trying again." });
66
+ } else if (res.status === 503) {
67
+ setError({ message: "Portal auth is not configured", hint: "Restart the dev server after updating environment variables." });
68
+ } else if (res.status >= 500) {
69
+ setError({ message: "Unable to sign in", hint: "The server returned an error. Try again in a moment." });
70
+ } else {
71
+ setError({ message: "Invalid username or password", hint: "Credentials are case-sensitive. Check for typos." });
72
+ }
73
+ } catch {
74
+ setError({ message: "Unable to connect", hint: "Check your internet connection and try again." });
75
+ }
76
+
77
+ setLoading(false);
78
+ }
79
+
80
+ const canSubmit = username.trim().length > 0 && password.length > 0 && !loading;
81
+
82
+ return (
83
+ <div className="relative flex min-h-screen items-center justify-center overflow-hidden bg-gray-100 px-4">
84
+ <div className="absolute -right-32 -top-32 h-96 w-96 rounded-full bg-primary/10 blur-[140px]" />
85
+ <div className="absolute -bottom-32 -left-32 h-96 w-96 rounded-full bg-primary/5 blur-[120px]" />
86
+
87
+ <div className="relative z-10 w-full max-w-sm">
88
+ <a href={companyUrl} className="mx-auto mb-8 flex w-fit items-center gap-2 transition-opacity hover:opacity-70">
89
+ {companyLogo}
90
+ <span className="text-base font-bold tracking-tight text-gray-900">{companyName}</span>
91
+ </a>
92
+
93
+ <div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
94
+ <div className="mb-5">
95
+ <h1 className="text-xl font-bold tracking-tight text-gray-900">{resolvedPortalLabel}</h1>
96
+ <p className="mt-1 text-sm leading-relaxed text-gray-500">
97
+ {resolvedDescription}
98
+ </p>
99
+ </div>
100
+
101
+ <form onSubmit={handleSubmit} className="space-y-4">
102
+ <div>
103
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700">Company Name</label>
104
+ <input
105
+ id="username"
106
+ type="text"
107
+ autoFocus
108
+ autoComplete="username"
109
+ autoCapitalize="off"
110
+ autoCorrect="off"
111
+ spellCheck={false}
112
+ value={username}
113
+ onChange={(e) => { setUsername(e.target.value); setError(null); }}
114
+ className="mt-1 block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
115
+ required
116
+ />
117
+ </div>
118
+ <div>
119
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">Password</label>
120
+ <div className="relative mt-1">
121
+ <input
122
+ id="password"
123
+ type={showPassword ? "text" : "password"}
124
+ autoComplete="current-password"
125
+ value={password}
126
+ onChange={(e) => { setPassword(e.target.value); setError(null); }}
127
+ className="block w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 pr-10 text-sm text-gray-900 shadow-sm focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary/30"
128
+ required
129
+ />
130
+ <button
131
+ type="button"
132
+ onClick={() => setShowPassword(!showPassword)}
133
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 cursor-pointer text-gray-400 transition-colors hover:text-gray-600"
134
+ tabIndex={-1}
135
+ aria-label={showPassword ? "Hide password" : "Show password"}
136
+ >
137
+ {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
138
+ </button>
139
+ </div>
140
+ </div>
141
+ {error && (
142
+ <div className="rounded-lg bg-red-50 px-3 py-2">
143
+ <p className="text-sm text-red-600">{error.message}</p>
144
+ {error.hint && <p className="mt-0.5 text-xs text-red-500/70">{error.hint}</p>}
145
+ </div>
146
+ )}
147
+ <button
148
+ type="submit"
149
+ disabled={!canSubmit}
150
+ className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-semibold text-white transition-all hover:bg-gray-800 active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-40"
151
+ >
152
+ {loading ? "Signing in..." : "Sign In"}
153
+ {!loading && <ArrowRight className="h-4 w-4" />}
154
+ </button>
155
+ </form>
156
+ </div>
157
+
158
+ <div className="mt-5 space-y-2 text-center text-xs">
159
+ <p className="text-gray-400">
160
+ Not a client? <a href={companyUrl} className="text-gray-500 underline underline-offset-2 transition-colors hover:text-gray-700">Visit {displayDomain}</a>
161
+ </p>
162
+ <p className="text-gray-400">
163
+ Lost your credentials? Email <span className="font-medium text-gray-500">{supportEmail}</span>
164
+ </p>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ );
169
+ }