showpane 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/meta/scaffold-manifest.json +5 -4
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/src/app/page.tsx +117 -133
- package/bundle/scaffold/src/components/copy-button.tsx +49 -0
- package/bundle/toolchain/VERSION +1 -1
- package/bundle/toolchain/bin/list-portals.ts +25 -9
- package/bundle/toolchain/skills/portal-create/SKILL.md +18 -0
- package/dist/index.js +29 -15
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
4
|
-
"scaffoldVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-04-09T14:11:15.147Z",
|
|
4
|
+
"scaffoldVersion": "0.2.2",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "0dd692f1c7e6bcabdf5dbdfe9abb73797d79d8e90da150d6098b63ddc695dc29",
|
|
7
7
|
".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
|
|
8
|
-
"VERSION": "
|
|
8
|
+
"VERSION": "998f6b887ed7a6ef44e84210d0237b715bed42ea7254f48dc418afec0a484103",
|
|
9
9
|
"docker-compose.yml": "420fd123da019c22f03662933537e24779b4c2c91f90c23abfec5965cd0f35ce",
|
|
10
10
|
"docker/Caddyfile": "d9c58086986795f5b3e42ff9b5942e60b8df946a1a0c40351381616c0b4d2bed",
|
|
11
11
|
"docker/Dockerfile": "340470e3735ea53b2c03003a13a91361652291add33c40a2bf13e6af2a8cb73a",
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"src/app/api/health/route.ts": "78fff55707372ce0cd6e9e49ef4f049622bc43cc42916d3f83e0162409d678b1",
|
|
48
48
|
"src/app/globals.css": "28dcda76006d0e6af01b6dcf1a315dc5b5b6931c880fc53fd6565ff09d5dd13a",
|
|
49
49
|
"src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
|
|
50
|
-
"src/app/page.tsx": "
|
|
50
|
+
"src/app/page.tsx": "df6abc817e751782df1f59ed770d6e0b47d3a24ea4ad3682485680bdf71845f8",
|
|
51
|
+
"src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
|
|
51
52
|
"src/components/portal-login.tsx": "8b0d91bb28674e1102fd2e5b5ddcc3a93755dd806fbd3d1b2dbea2646cffca5e",
|
|
52
53
|
"src/components/portal-shell.tsx": "a4e16e118ef93f79e71fb69e80f1fac6e6fff90f0fbdacdf8deb821a57656877",
|
|
53
54
|
"src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
|
package/bundle/scaffold/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.2
|
|
@@ -1,177 +1,161 @@
|
|
|
1
|
+
import { CopyButton } from "@/components/copy-button";
|
|
2
|
+
import { resolveDefaultOrganizationId } from "@/lib/client-portals";
|
|
1
3
|
import { prisma } from "@/lib/db";
|
|
2
4
|
import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
|
|
5
|
+
import { ArrowUpRight, BookOpen, Command, MessageSquareQuote } from "lucide-react";
|
|
3
6
|
import Link from "next/link";
|
|
4
|
-
import { Presentation, Briefcase, UserPlus } from "lucide-react";
|
|
5
7
|
import os from "node:os";
|
|
6
8
|
import path from "node:path";
|
|
7
9
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
},
|
|
10
|
+
const GUIDE_URL = "https://app.showpane.com/docs/first-portal";
|
|
11
|
+
const PROMPT_EXAMPLES = [
|
|
12
|
+
"Create a portal for my client Acme based on my call transcript, which is here: [paste it]",
|
|
13
|
+
"Create a portal for my client Acme based on my call from earlier today. Use the Granola MCP to grab the transcript.",
|
|
24
14
|
];
|
|
25
15
|
|
|
26
16
|
export default async function Home() {
|
|
27
17
|
let portalCount = 0;
|
|
28
18
|
const showpaneBinDir = path.join(os.homedir(), ".showpane", "bin");
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
const prefersCanonicalCommand = (process.env.PATH ?? "").split(path.delimiter).includes(showpaneBinDir);
|
|
20
|
+
const primaryCommand = "showpane claude";
|
|
21
|
+
const fallbackCommand = "npx showpane claude";
|
|
32
22
|
try {
|
|
33
23
|
if (isRuntimeSnapshotMode()) {
|
|
34
24
|
const state = await getRuntimeState();
|
|
35
25
|
portalCount = state?.portals.length ?? 0;
|
|
36
26
|
} else {
|
|
37
|
-
|
|
27
|
+
const organizationId = await resolveDefaultOrganizationId();
|
|
28
|
+
if (organizationId) {
|
|
29
|
+
portalCount = await prisma.clientPortal.count({
|
|
30
|
+
where: { organizationId },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
38
33
|
}
|
|
39
34
|
} catch {
|
|
40
35
|
// DB not ready yet — show welcome page with 0 portals
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
return (
|
|
44
|
-
<main className="min-h-screen
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝`}
|
|
39
|
+
<main className="min-h-screen bg-[#f6f1e8] text-slate-900">
|
|
40
|
+
<div className="bg-[radial-gradient(circle_at_top,_rgba(255,255,255,0.24),_transparent_48%),linear-gradient(180deg,_#214668_0%,_#3d6f9c_100%)] px-4 py-16 text-white sm:py-20">
|
|
41
|
+
<div className="mx-auto max-w-3xl">
|
|
42
|
+
<div className="inline-flex items-center rounded-full border border-white/20 bg-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-white/80">
|
|
43
|
+
Local app running
|
|
67
44
|
</div>
|
|
68
|
-
<
|
|
69
|
-
|
|
45
|
+
<h1 className="mt-6 text-4xl font-semibold tracking-tight sm:text-5xl">
|
|
46
|
+
Your Showpane workspace is ready
|
|
47
|
+
</h1>
|
|
48
|
+
<p className="mt-4 max-w-2xl text-base leading-7 text-white/82 sm:text-lg">
|
|
49
|
+
Open a new terminal window, run Showpane with Claude, and create your first client portal.
|
|
70
50
|
</p>
|
|
71
51
|
</div>
|
|
72
52
|
</div>
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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>
|
|
54
|
+
<div className="px-4 py-10 sm:py-12">
|
|
55
|
+
<div className="mx-auto max-w-3xl space-y-5">
|
|
56
|
+
<section className="rounded-[28px] border border-slate-200 bg-slate-950 p-6 text-white shadow-[0_24px_80px_rgba(15,23,42,0.16)] sm:p-7">
|
|
57
|
+
<div className="flex items-start justify-between gap-4">
|
|
58
|
+
<div>
|
|
59
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-white/80">
|
|
60
|
+
<Command className="h-4 w-4" />
|
|
61
|
+
Start with Claude
|
|
91
62
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
63
|
+
<p className="mt-3 text-sm leading-6 text-white/72">
|
|
64
|
+
Open a new terminal window and run this command there. Your current terminal is running the local app, so this command belongs in a fresh one.
|
|
65
|
+
</p>
|
|
66
|
+
{!prefersCanonicalCommand && (
|
|
67
|
+
<p className="mt-2 text-xs leading-5 text-white/60">
|
|
68
|
+
If <code className="font-mono text-white">{primaryCommand}</code> isn't available in your shell yet, use{" "}
|
|
69
|
+
<code className="font-mono text-white">{fallbackCommand}</code>.
|
|
103
70
|
</p>
|
|
104
|
-
|
|
105
|
-
/portal create acme-health
|
|
106
|
-
</code>
|
|
107
|
-
</div>
|
|
71
|
+
)}
|
|
108
72
|
</div>
|
|
109
|
-
|
|
73
|
+
<CopyButton text={primaryCommand} invert />
|
|
74
|
+
</div>
|
|
110
75
|
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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>
|
|
76
|
+
<div className="mt-5 rounded-2xl border border-white/10 bg-white/5 p-4">
|
|
77
|
+
<code className="block overflow-x-auto font-mono text-sm text-white sm:text-[15px]">
|
|
78
|
+
{primaryCommand}
|
|
79
|
+
</code>
|
|
80
|
+
</div>
|
|
81
|
+
</section>
|
|
127
82
|
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
target="_blank"
|
|
134
|
-
rel="noopener noreferrer"
|
|
135
|
-
>
|
|
136
|
-
Install it here
|
|
137
|
-
</a>
|
|
138
|
-
</p>
|
|
83
|
+
<section className="rounded-[28px] border border-slate-200 bg-white p-6 shadow-[0_20px_70px_rgba(15,23,42,0.07)] sm:p-7">
|
|
84
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
|
85
|
+
<MessageSquareQuote className="h-4 w-4" />
|
|
86
|
+
What to say to Claude
|
|
87
|
+
</div>
|
|
139
88
|
|
|
140
|
-
|
|
141
|
-
|
|
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) => (
|
|
89
|
+
<div className="mt-5 space-y-3">
|
|
90
|
+
{PROMPT_EXAMPLES.map((example, index) => (
|
|
147
91
|
<div
|
|
148
|
-
key={
|
|
149
|
-
className="border border-
|
|
92
|
+
key={example}
|
|
93
|
+
className="rounded-2xl border border-slate-200 bg-slate-50 p-4"
|
|
150
94
|
>
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
95
|
+
<div className="flex items-start justify-between gap-3">
|
|
96
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
|
97
|
+
Example {index + 1}
|
|
98
|
+
</p>
|
|
99
|
+
<CopyButton text={example} />
|
|
100
|
+
</div>
|
|
101
|
+
<p className="mt-3 whitespace-pre-wrap pr-2 font-mono text-sm leading-6 text-slate-700">
|
|
102
|
+
{example}
|
|
103
|
+
</p>
|
|
154
104
|
</div>
|
|
155
105
|
))}
|
|
156
106
|
</div>
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
<section className="rounded-[32px] border border-[#b8d2ee] bg-[linear-gradient(135deg,_#f8fcff_0%,_#d8ebfb_55%,_#bed8f1_100%)] p-6 shadow-[0_24px_90px_rgba(61,111,156,0.18)] sm:p-8">
|
|
110
|
+
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
|
111
|
+
<div className="max-w-xl">
|
|
112
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-[#214668]">
|
|
113
|
+
<BookOpen className="h-4 w-4" />
|
|
114
|
+
Need help creating your first portal?
|
|
115
|
+
</div>
|
|
116
|
+
<p className="mt-3 text-base leading-7 text-[#284f74]">
|
|
117
|
+
Follow the step-by-step guide with examples, best practices, and a walkthrough video.
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<a
|
|
121
|
+
href={GUIDE_URL}
|
|
122
|
+
className="inline-flex items-center justify-center gap-2 rounded-full bg-[#214668] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#18344d]"
|
|
123
|
+
target="_blank"
|
|
124
|
+
rel="noopener noreferrer"
|
|
125
|
+
>
|
|
126
|
+
Open Creating Your First Portal
|
|
127
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
128
|
+
</a>
|
|
129
|
+
</div>
|
|
130
|
+
</section>
|
|
131
|
+
|
|
132
|
+
<div className="space-y-3 pt-2 text-center">
|
|
133
|
+
<p className="text-xs text-slate-500">
|
|
134
|
+
Need Claude Code first?{" "}
|
|
135
|
+
<a
|
|
136
|
+
href="https://claude.ai/code"
|
|
137
|
+
className="font-medium text-[#214668] underline decoration-slate-300 underline-offset-4 hover:decoration-[#214668]"
|
|
138
|
+
target="_blank"
|
|
139
|
+
rel="noopener noreferrer"
|
|
140
|
+
>
|
|
141
|
+
Install it here
|
|
142
|
+
</a>
|
|
143
|
+
</p>
|
|
144
|
+
|
|
145
|
+
<div className="space-y-1 text-xs text-slate-400">
|
|
146
|
+
{portalCount > 0 && (
|
|
147
|
+
<p>
|
|
148
|
+
You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
|
|
149
|
+
<Link href="/client" className="text-[#214668] hover:underline">
|
|
150
|
+
Go to login
|
|
151
|
+
</Link>
|
|
152
|
+
</p>
|
|
153
|
+
)}
|
|
154
|
+
<p>Powered by Claude Code</p>
|
|
155
|
+
</div>
|
|
157
156
|
</div>
|
|
158
157
|
</div>
|
|
159
158
|
</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
159
|
</main>
|
|
176
160
|
);
|
|
177
161
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, Copy } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
|
|
6
|
+
type CopyButtonProps = {
|
|
7
|
+
text: string;
|
|
8
|
+
invert?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function CopyButton({ text, invert = false }: CopyButtonProps) {
|
|
12
|
+
const [copied, setCopied] = useState(false);
|
|
13
|
+
const [copyError, setCopyError] = useState(false);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!copied && !copyError) return;
|
|
17
|
+
|
|
18
|
+
const timeout = window.setTimeout(() => {
|
|
19
|
+
setCopied(false);
|
|
20
|
+
setCopyError(false);
|
|
21
|
+
}, 2000);
|
|
22
|
+
|
|
23
|
+
return () => window.clearTimeout(timeout);
|
|
24
|
+
}, [copied, copyError]);
|
|
25
|
+
|
|
26
|
+
async function handleCopy() {
|
|
27
|
+
try {
|
|
28
|
+
await navigator.clipboard.writeText(text);
|
|
29
|
+
setCopied(true);
|
|
30
|
+
} catch {
|
|
31
|
+
setCopyError(true);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={handleCopy}
|
|
39
|
+
className={
|
|
40
|
+
invert
|
|
41
|
+
? "inline-flex items-center gap-1.5 rounded-full border border-white/20 bg-white/10 px-3 py-1.5 text-xs font-semibold text-white transition hover:bg-white/20"
|
|
42
|
+
: "inline-flex items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 transition hover:border-slate-300 hover:bg-slate-50"
|
|
43
|
+
}
|
|
44
|
+
>
|
|
45
|
+
{copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
|
|
46
|
+
{copied ? "Copied!" : copyError ? "Failed" : "Copy"}
|
|
47
|
+
</button>
|
|
48
|
+
);
|
|
49
|
+
}
|
package/bundle/toolchain/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.2 (requires app >= 0.2.2)
|
|
@@ -9,30 +9,46 @@ async function main() {
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
|
|
11
11
|
if (args.includes("--help")) {
|
|
12
|
-
console.log("Usage: list-portals [--org-id <orgId>]");
|
|
13
|
-
console.log("Lists all client portals. Defaults to first organization if
|
|
12
|
+
console.log("Usage: list-portals [--org-id <orgId>] [--org-slug <slug>]");
|
|
13
|
+
console.log("Lists all client portals. Defaults to first organization if no org filter is provided.");
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
const prisma = new PrismaClient();
|
|
18
18
|
try {
|
|
19
19
|
const getArg = (flag: string) => { const i = args.indexOf(flag); return i !== -1 ? args[i + 1] : undefined; };
|
|
20
|
-
|
|
20
|
+
const orgId = getArg("--org-id");
|
|
21
|
+
const orgSlug = getArg("--org-slug");
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const organization = orgId
|
|
24
|
+
? await prisma.organization.findUnique({
|
|
25
|
+
where: { id: orgId },
|
|
26
|
+
select: { id: true, name: true, slug: true },
|
|
27
|
+
})
|
|
28
|
+
: orgSlug
|
|
29
|
+
? await prisma.organization.findUnique({
|
|
30
|
+
where: { slug: orgSlug },
|
|
31
|
+
select: { id: true, name: true, slug: true },
|
|
32
|
+
})
|
|
33
|
+
: await prisma.organization.findFirst({
|
|
34
|
+
orderBy: { createdAt: "asc" },
|
|
35
|
+
select: { id: true, name: true, slug: true },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!organization) fail("No organizations found");
|
|
27
39
|
|
|
28
40
|
const portals = await prisma.clientPortal.findMany({
|
|
29
|
-
where: { organizationId:
|
|
41
|
+
where: { organizationId: organization.id },
|
|
30
42
|
include: { organization: { select: { name: true } } },
|
|
31
43
|
orderBy: { createdAt: "desc" },
|
|
32
44
|
});
|
|
33
45
|
|
|
34
46
|
console.log(JSON.stringify({
|
|
35
47
|
ok: true,
|
|
48
|
+
orgId: organization.id,
|
|
49
|
+
orgSlug: organization.slug,
|
|
50
|
+
orgName: organization.name,
|
|
51
|
+
total: portals.length,
|
|
36
52
|
portals: portals.map((p) => ({
|
|
37
53
|
slug: p.slug,
|
|
38
54
|
companyName: p.companyName,
|
|
@@ -71,6 +71,24 @@ mention them unless they directly affect the current task.
|
|
|
71
71
|
|
|
72
72
|
## Steps
|
|
73
73
|
|
|
74
|
+
### Step 0: Check first-portal guide eligibility
|
|
75
|
+
|
|
76
|
+
Before asking for the slug or generating anything, determine the existing portal count
|
|
77
|
+
for the active organization:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" --org-slug "$ORG_SLUG"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The script returns JSON with `total` and `orgId` for the active org. Reuse that
|
|
84
|
+
`orgId` for the slug validation and portal creation commands below.
|
|
85
|
+
|
|
86
|
+
- If `total` is `0`, `1`, or `2`, say this line exactly once:
|
|
87
|
+
`If helpful, the first-portal guide has examples and best practices: https://app.showpane.com/docs/first-portal`
|
|
88
|
+
- If `total` is `3` or higher, do not mention the guide.
|
|
89
|
+
- Mention it once per invocation only. Do not repeat it later in the same flow.
|
|
90
|
+
- Use the org-scoped portal count directly. Do not use learnings or timeline heuristics.
|
|
91
|
+
|
|
74
92
|
### Step 1: Determine the portal slug
|
|
75
93
|
|
|
76
94
|
If the user provided a slug (e.g., `/portal create acme-health`), use it. Otherwise, infer from context — the company name mentioned in conversation, a meeting transcript, or ask the user directly.
|
package/dist/index.js
CHANGED
|
@@ -331,6 +331,18 @@ function stepFailure(label, errorLike, hint) {
|
|
|
331
331
|
}
|
|
332
332
|
process.exit(1);
|
|
333
333
|
}
|
|
334
|
+
function shouldUseSpinner(verbose) {
|
|
335
|
+
return process.stdout.isTTY && !verbose;
|
|
336
|
+
}
|
|
337
|
+
function stepStartForCreate(label, options) {
|
|
338
|
+
stepStart(label, shouldUseSpinner(options.verbose));
|
|
339
|
+
}
|
|
340
|
+
function stepSuccessForCreate(label) {
|
|
341
|
+
stepSuccess(label);
|
|
342
|
+
}
|
|
343
|
+
function stepFailureForCreate(label, errorLike, hint) {
|
|
344
|
+
stepFailure(label, errorLike, hint);
|
|
345
|
+
}
|
|
334
346
|
function attachSpinnerCleanup() {
|
|
335
347
|
const cleanup = () => stopSpinner();
|
|
336
348
|
process.on("exit", cleanup);
|
|
@@ -792,9 +804,11 @@ function printCreateSuccessCard(projectRoot, url) {
|
|
|
792
804
|
console.log(` ${BOLD}App:${RESET} ${url}`);
|
|
793
805
|
console.log(` ${BOLD}Demo:${RESET} example / demo-only-password`);
|
|
794
806
|
console.log();
|
|
795
|
-
console.log(` ${BOLD}Next:${RESET}`);
|
|
807
|
+
console.log(` ${BOLD}Next (in a new terminal window):${RESET}`);
|
|
796
808
|
console.log(` ${DIM}${resumeCommand}${RESET}`);
|
|
797
809
|
console.log();
|
|
810
|
+
console.log(` ${DIM}Your current terminal is running the local app logs, so open a fresh terminal before you run that command.${RESET}`);
|
|
811
|
+
console.log();
|
|
798
812
|
console.log(` ${BOLD}Try:${RESET}`);
|
|
799
813
|
console.log(` ${DIM}Create a portal for my call with Acme Health${RESET}`);
|
|
800
814
|
if (resumeHint) {
|
|
@@ -1046,19 +1060,19 @@ async function createProject(args) {
|
|
|
1046
1060
|
console.log();
|
|
1047
1061
|
blue(`Setting up ${BOLD}${companyName}${RESET} portal as ${DIM}${dirName}/${RESET}`);
|
|
1048
1062
|
console.log();
|
|
1049
|
-
|
|
1063
|
+
stepStartForCreate("Create project", options);
|
|
1050
1064
|
try {
|
|
1051
1065
|
copyScaffoldFiles(join(bundleRoot, "scaffold"), projectRoot, scaffoldManifest);
|
|
1052
|
-
|
|
1066
|
+
stepSuccessForCreate("Project created");
|
|
1053
1067
|
} catch (errorLike) {
|
|
1054
|
-
|
|
1068
|
+
stepFailureForCreate("Create project", errorLike);
|
|
1055
1069
|
}
|
|
1056
|
-
|
|
1070
|
+
stepStartForCreate("Install dependencies", options);
|
|
1057
1071
|
try {
|
|
1058
1072
|
installDependencies(projectRoot, options.verbose);
|
|
1059
|
-
|
|
1073
|
+
stepSuccessForCreate("Dependencies installed");
|
|
1060
1074
|
} catch (errorLike) {
|
|
1061
|
-
|
|
1075
|
+
stepFailureForCreate(
|
|
1062
1076
|
"Install dependencies",
|
|
1063
1077
|
errorLike,
|
|
1064
1078
|
"Check your Node.js version and network connection, then try again."
|
|
@@ -1072,19 +1086,19 @@ async function createProject(args) {
|
|
|
1072
1086
|
AUTH_SECRET="${authSecret}"
|
|
1073
1087
|
`
|
|
1074
1088
|
);
|
|
1075
|
-
|
|
1089
|
+
stepStartForCreate("Configure database", options);
|
|
1076
1090
|
try {
|
|
1077
1091
|
generateLocalDatabase(projectRoot, databaseUrl, options.verbose);
|
|
1078
1092
|
seedProject(projectRoot, databaseUrl, options.verbose);
|
|
1079
|
-
|
|
1093
|
+
stepSuccessForCreate("Database configured");
|
|
1080
1094
|
} catch (errorLike) {
|
|
1081
|
-
|
|
1095
|
+
stepFailureForCreate(
|
|
1082
1096
|
"Configure database",
|
|
1083
1097
|
errorLike,
|
|
1084
1098
|
"Check Prisma setup and the generated .env file, then retry the install."
|
|
1085
1099
|
);
|
|
1086
1100
|
}
|
|
1087
|
-
|
|
1101
|
+
stepStartForCreate("Install Claude skills", options);
|
|
1088
1102
|
let toolchainInfo;
|
|
1089
1103
|
try {
|
|
1090
1104
|
toolchainInfo = syncToolchain(bundleRoot, showpaneVersion, false);
|
|
@@ -1102,15 +1116,15 @@ AUTH_SECRET="${authSecret}"
|
|
|
1102
1116
|
toolchainInfo.toolchainVersion
|
|
1103
1117
|
);
|
|
1104
1118
|
tryInitializeGitRepo(projectRoot, false);
|
|
1105
|
-
|
|
1119
|
+
stepSuccessForCreate("Claude skills installed");
|
|
1106
1120
|
} catch (errorLike) {
|
|
1107
|
-
|
|
1121
|
+
stepFailureForCreate(
|
|
1108
1122
|
"Install Claude skills",
|
|
1109
1123
|
errorLike,
|
|
1110
1124
|
"Check permissions for ~/.showpane and ~/.claude/skills, then try again."
|
|
1111
1125
|
);
|
|
1112
1126
|
}
|
|
1113
|
-
|
|
1127
|
+
stepStartForCreate("Start app", options);
|
|
1114
1128
|
let serverStart;
|
|
1115
1129
|
try {
|
|
1116
1130
|
serverStart = await startDevServer(
|
|
@@ -1120,7 +1134,7 @@ AUTH_SECRET="${authSecret}"
|
|
|
1120
1134
|
options.verbose
|
|
1121
1135
|
);
|
|
1122
1136
|
} catch (errorLike) {
|
|
1123
|
-
|
|
1137
|
+
stepFailureForCreate(
|
|
1124
1138
|
"Start app",
|
|
1125
1139
|
errorLike,
|
|
1126
1140
|
`Run ${BOLD}cd ${dirName} && npm run dev${RESET} for more detail.`
|