showpane 0.4.2 → 0.4.4
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/README.md +12 -0
- package/bundle/meta/scaffold-manifest.json +5 -4
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/src/app/page.tsx +114 -130
- 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 +403 -26
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,18 @@ Flags:
|
|
|
24
24
|
Authenticate with Showpane Cloud for hosted portal deployment.
|
|
25
25
|
This is auth only — org creation and billing happen in the Showpane Cloud web app.
|
|
26
26
|
|
|
27
|
+
### `showpane claude`
|
|
28
|
+
Resume your Showpane workspace by launching Claude Code in the right project directory.
|
|
29
|
+
|
|
30
|
+
Flags:
|
|
31
|
+
|
|
32
|
+
- `--project <name-or-path>` — open a specific remembered workspace
|
|
33
|
+
- `--yes` / `--name <company>` — only used when no workspace exists yet and Showpane needs to create the first one
|
|
34
|
+
- `--verbose` — show raw setup logs during first-time onboarding
|
|
35
|
+
|
|
36
|
+
### `showpane projects`
|
|
37
|
+
List remembered Showpane workspaces and show which one is currently active for global skills.
|
|
38
|
+
|
|
27
39
|
### `showpane sync`
|
|
28
40
|
Install or refresh the global Showpane toolchain and Claude Code skills for the current CLI version.
|
|
29
41
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
4
|
-
"scaffoldVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-04-09T13:24:17.013Z",
|
|
4
|
+
"scaffoldVersion": "0.2.1",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "0dd692f1c7e6bcabdf5dbdfe9abb73797d79d8e90da150d6098b63ddc695dc29",
|
|
7
7
|
".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
|
|
8
|
-
"VERSION": "
|
|
8
|
+
"VERSION": "71015c979ccb0fc8a0be7ca0ae83046ab045cdc2c8faa09fb2f0f7e440f9b4a6",
|
|
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": "1c48a37632621373db7730756aacde708e29d6f1bd14062b63736fe8057ce842",
|
|
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.1
|
|
@@ -1,171 +1,155 @@
|
|
|
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
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
5
9
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
icon: Presentation,
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
name: "Consulting",
|
|
14
|
-
description: "Project overview, deliverables, timeline",
|
|
15
|
-
icon: Briefcase,
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
name: "Onboarding",
|
|
19
|
-
description: "Welcome, setup steps, resources",
|
|
20
|
-
icon: UserPlus,
|
|
21
|
-
},
|
|
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.",
|
|
22
14
|
];
|
|
23
15
|
|
|
24
16
|
export default async function Home() {
|
|
25
17
|
let portalCount = 0;
|
|
18
|
+
const showpaneBinDir = path.join(os.homedir(), ".showpane", "bin");
|
|
19
|
+
const resumeCommand = (process.env.PATH ?? "").split(path.delimiter).includes(showpaneBinDir)
|
|
20
|
+
? "showpane claude"
|
|
21
|
+
: "npx showpane claude";
|
|
26
22
|
try {
|
|
27
23
|
if (isRuntimeSnapshotMode()) {
|
|
28
24
|
const state = await getRuntimeState();
|
|
29
25
|
portalCount = state?.portals.length ?? 0;
|
|
30
26
|
} else {
|
|
31
|
-
|
|
27
|
+
const organizationId = await resolveDefaultOrganizationId();
|
|
28
|
+
if (organizationId) {
|
|
29
|
+
portalCount = await prisma.clientPortal.count({
|
|
30
|
+
where: { organizationId },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
32
33
|
}
|
|
33
34
|
} catch {
|
|
34
35
|
// DB not ready yet — show welcome page with 0 portals
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
return (
|
|
38
|
-
<main className="min-h-screen
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
style={{
|
|
44
|
-
backgroundImage: "radial-gradient(circle, white 1px, transparent 1px)",
|
|
45
|
-
backgroundSize: "24px 24px",
|
|
46
|
-
}}
|
|
47
|
-
/>
|
|
48
|
-
<div className="relative">
|
|
49
|
-
<h1 className="sr-only">SHOWPANE</h1>
|
|
50
|
-
<div
|
|
51
|
-
role="img"
|
|
52
|
-
aria-label="SHOWPANE"
|
|
53
|
-
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"
|
|
54
|
-
>
|
|
55
|
-
{`███████╗██╗ ██╗ ██████╗ ██╗ ██╗██████╗ █████╗ ███╗ ██╗███████╗
|
|
56
|
-
██╔════╝██║ ██║██╔═══██╗██║ ██║██╔══██╗██╔══██╗████╗ ██║██╔════╝
|
|
57
|
-
███████╗███████║██║ ██║██║ █╗ ██║██████╔╝███████║██╔██╗ ██║█████╗
|
|
58
|
-
╚════██║██╔══██║██║ ██║██║███╗██║██╔═══╝ ██╔══██║██║╚██╗██║██╔══╝
|
|
59
|
-
███████║██║ ██║╚██████╔╝╚███╔███╔╝██║ ██║ ██║██║ ╚████║███████╗
|
|
60
|
-
╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══╝╚══╝╚═╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝`}
|
|
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
|
|
61
44
|
</div>
|
|
62
|
-
<
|
|
63
|
-
|
|
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 Claude in your Showpane workspace and create your first client portal.
|
|
64
50
|
</p>
|
|
65
51
|
</div>
|
|
66
52
|
</div>
|
|
67
53
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
1
|
|
77
|
-
</span>
|
|
78
|
-
<div className="min-w-0">
|
|
79
|
-
<p className="text-gray-900 font-medium mb-2">
|
|
80
|
-
Open your terminal in this directory
|
|
81
|
-
</p>
|
|
82
|
-
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
|
|
83
|
-
cd app
|
|
84
|
-
</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
|
|
85
62
|
</div>
|
|
63
|
+
<p className="mt-3 text-sm leading-6 text-white/72">
|
|
64
|
+
This opens Claude in the right Showpane workspace so you can start creating portals immediately.
|
|
65
|
+
</p>
|
|
86
66
|
</div>
|
|
87
|
-
|
|
67
|
+
<CopyButton text={resumeCommand} invert />
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="mt-5 rounded-2xl border border-white/10 bg-white/5 p-4">
|
|
71
|
+
<code className="block overflow-x-auto font-mono text-sm text-white sm:text-[15px]">
|
|
72
|
+
{resumeCommand}
|
|
73
|
+
</code>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<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">
|
|
78
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
|
79
|
+
<MessageSquareQuote className="h-4 w-4" />
|
|
80
|
+
What to say to Claude
|
|
81
|
+
</div>
|
|
88
82
|
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
83
|
+
<div className="mt-5 space-y-3">
|
|
84
|
+
{PROMPT_EXAMPLES.map((example, index) => (
|
|
85
|
+
<div
|
|
86
|
+
key={example}
|
|
87
|
+
className="rounded-2xl border border-slate-200 bg-slate-50 p-4"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex items-start justify-between gap-3">
|
|
90
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
|
91
|
+
Example {index + 1}
|
|
92
|
+
</p>
|
|
93
|
+
<CopyButton text={example} />
|
|
94
|
+
</div>
|
|
95
|
+
<p className="mt-3 whitespace-pre-wrap pr-2 font-mono text-sm leading-6 text-slate-700">
|
|
96
|
+
{example}
|
|
97
97
|
</p>
|
|
98
|
-
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded">
|
|
99
|
-
claude
|
|
100
|
-
</code>
|
|
101
98
|
</div>
|
|
102
|
-
|
|
103
|
-
</
|
|
99
|
+
))}
|
|
100
|
+
</div>
|
|
101
|
+
</section>
|
|
104
102
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<p className="text-gray-900 font-medium mb-2">
|
|
112
|
-
Tell it what to create
|
|
113
|
-
</p>
|
|
114
|
-
<code className="block text-sm text-gray-300 font-mono bg-[#111827] px-3 py-2 rounded overflow-x-auto">
|
|
115
|
-
Create a portal for my call with [client name]
|
|
116
|
-
</code>
|
|
103
|
+
<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">
|
|
104
|
+
<div className="flex flex-col gap-6 sm:flex-row sm:items-start sm:justify-between">
|
|
105
|
+
<div className="max-w-xl">
|
|
106
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-[#214668]">
|
|
107
|
+
<BookOpen className="h-4 w-4" />
|
|
108
|
+
Need help creating your first portal?
|
|
117
109
|
</div>
|
|
110
|
+
<p className="mt-3 text-base leading-7 text-[#284f74]">
|
|
111
|
+
Follow the step-by-step guide with examples, best practices, and a walkthrough video.
|
|
112
|
+
</p>
|
|
118
113
|
</div>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Install it here
|
|
131
|
-
</a>
|
|
132
|
-
</p>
|
|
114
|
+
<a
|
|
115
|
+
href={GUIDE_URL}
|
|
116
|
+
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]"
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noopener noreferrer"
|
|
119
|
+
>
|
|
120
|
+
Open Creating Your First Portal
|
|
121
|
+
<ArrowUpRight className="h-4 w-4" />
|
|
122
|
+
</a>
|
|
123
|
+
</div>
|
|
124
|
+
</section>
|
|
133
125
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
126
|
+
<div className="space-y-3 pt-2 text-center">
|
|
127
|
+
<p className="text-xs text-slate-500">
|
|
128
|
+
Need Claude Code first?{" "}
|
|
129
|
+
<a
|
|
130
|
+
href="https://claude.ai/code"
|
|
131
|
+
className="font-medium text-[#214668] underline decoration-slate-300 underline-offset-4 hover:decoration-[#214668]"
|
|
132
|
+
target="_blank"
|
|
133
|
+
rel="noopener noreferrer"
|
|
134
|
+
>
|
|
135
|
+
Install it here
|
|
136
|
+
</a>
|
|
138
137
|
</p>
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
|
|
139
|
+
<div className="space-y-1 text-xs text-slate-400">
|
|
140
|
+
{portalCount > 0 && (
|
|
141
|
+
<p>
|
|
142
|
+
You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
|
|
143
|
+
<Link href="/client" className="text-[#214668] hover:underline">
|
|
144
|
+
Go to login
|
|
145
|
+
</Link>
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
<p>Powered by Claude Code</p>
|
|
150
149
|
</div>
|
|
151
150
|
</div>
|
|
152
151
|
</div>
|
|
153
152
|
</div>
|
|
154
|
-
|
|
155
|
-
{/* Footer zone */}
|
|
156
|
-
<footer className="bg-[#FDFBF7] px-4 pb-8 text-center space-y-2">
|
|
157
|
-
{portalCount > 0 && (
|
|
158
|
-
<p className="text-sm text-gray-500">
|
|
159
|
-
You have {portalCount} portal{portalCount !== 1 ? "s" : ""}.{" "}
|
|
160
|
-
<Link href="/client" className="text-blue-600 hover:underline">
|
|
161
|
-
Go to login
|
|
162
|
-
</Link>
|
|
163
|
-
</p>
|
|
164
|
-
)}
|
|
165
|
-
<p className="text-xs text-gray-400">
|
|
166
|
-
Powered by Claude Code
|
|
167
|
-
</p>
|
|
168
|
-
</footer>
|
|
169
153
|
</main>
|
|
170
154
|
);
|
|
171
155
|
}
|
|
@@ -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.1 (requires app >= 0.2.1)
|
|
@@ -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
|
@@ -23,7 +23,7 @@ var {
|
|
|
23
23
|
unlinkSync,
|
|
24
24
|
writeFileSync
|
|
25
25
|
} = fs;
|
|
26
|
-
var { dirname, join, resolve } = path;
|
|
26
|
+
var { basename, dirname, join, resolve } = path;
|
|
27
27
|
var { homedir } = os;
|
|
28
28
|
var RESET = "\x1B[0m";
|
|
29
29
|
var BOLD = "\x1B[1m";
|
|
@@ -34,6 +34,7 @@ var WHITE = "\x1B[37m";
|
|
|
34
34
|
var RED = "\x1B[31m";
|
|
35
35
|
var API_BASE = "https://app.showpane.com";
|
|
36
36
|
var SHOWPANE_HOME = join(homedir(), ".showpane");
|
|
37
|
+
var SHOWPANE_BIN_DIR = join(SHOWPANE_HOME, "bin");
|
|
37
38
|
var TOOLCHAIN_DIR = join(SHOWPANE_HOME, "toolchains");
|
|
38
39
|
var CURRENT_TOOLCHAIN_LINK = join(SHOWPANE_HOME, "current");
|
|
39
40
|
var CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
@@ -61,6 +62,9 @@ function error(message) {
|
|
|
61
62
|
function printCreateUsage() {
|
|
62
63
|
console.log("Usage: showpane [--yes --name <company>] [--no-open] [--verbose]");
|
|
63
64
|
}
|
|
65
|
+
function printClaudeUsage() {
|
|
66
|
+
console.log("Usage: showpane claude [--project <name-or-path>] [--yes --name <company>] [--verbose]");
|
|
67
|
+
}
|
|
64
68
|
function printBanner() {
|
|
65
69
|
const banner = `
|
|
66
70
|
${BOLD}${WHITE} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
@@ -121,6 +125,51 @@ function parseCreateArgs(args) {
|
|
|
121
125
|
}
|
|
122
126
|
return options;
|
|
123
127
|
}
|
|
128
|
+
function parseClaudeArgs(args) {
|
|
129
|
+
const options = {
|
|
130
|
+
noOpen: false,
|
|
131
|
+
verbose: false,
|
|
132
|
+
yes: false
|
|
133
|
+
};
|
|
134
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
135
|
+
const arg = args[index];
|
|
136
|
+
if (arg === "--yes") {
|
|
137
|
+
options.yes = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--no-open") {
|
|
141
|
+
options.noOpen = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg === "--verbose") {
|
|
145
|
+
options.verbose = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === "--name") {
|
|
149
|
+
const value = args[index + 1];
|
|
150
|
+
if (!value || value.startsWith("--")) {
|
|
151
|
+
throw new Error("Missing value for --name.");
|
|
152
|
+
}
|
|
153
|
+
options.companyName = value.trim();
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (arg === "--project") {
|
|
158
|
+
const value = args[index + 1];
|
|
159
|
+
if (!value || value.startsWith("--")) {
|
|
160
|
+
throw new Error("Missing value for --project.");
|
|
161
|
+
}
|
|
162
|
+
options.project = value.trim();
|
|
163
|
+
index += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
167
|
+
}
|
|
168
|
+
if (options.project && options.companyName) {
|
|
169
|
+
throw new Error("`--project` can not be combined with `--name`.");
|
|
170
|
+
}
|
|
171
|
+
return options;
|
|
172
|
+
}
|
|
124
173
|
function toSlug(name) {
|
|
125
174
|
return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
126
175
|
}
|
|
@@ -179,6 +228,21 @@ function maybePrintShowpaneUpdateMessage(currentVersion) {
|
|
|
179
228
|
} catch {
|
|
180
229
|
}
|
|
181
230
|
}
|
|
231
|
+
function normalizePathForComparison(targetPath) {
|
|
232
|
+
const normalized = path.normalize(resolve(targetPath));
|
|
233
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
234
|
+
}
|
|
235
|
+
function isShowpaneShimOnPath() {
|
|
236
|
+
const pathValue = process.env.PATH ?? "";
|
|
237
|
+
const binDir = normalizePathForComparison(SHOWPANE_BIN_DIR);
|
|
238
|
+
return pathValue.split(path.delimiter).filter(Boolean).some((entry) => normalizePathForComparison(entry) === binDir);
|
|
239
|
+
}
|
|
240
|
+
function getResumeCommand() {
|
|
241
|
+
return isShowpaneShimOnPath() ? "showpane claude" : "npx showpane claude";
|
|
242
|
+
}
|
|
243
|
+
function getResumeHint() {
|
|
244
|
+
return isShowpaneShimOnPath() ? null : `Optional: add ${SHOWPANE_BIN_DIR} to your PATH to use ${BOLD}showpane${RESET} directly.`;
|
|
245
|
+
}
|
|
182
246
|
function getCommandOutput(errorLike) {
|
|
183
247
|
const error2 = errorLike;
|
|
184
248
|
const stdout = typeof error2?.stdout === "string" ? error2.stdout : error2?.stdout?.toString() ?? "";
|
|
@@ -209,13 +273,48 @@ function runInstallerCommand(command2, cwd, env, verbose) {
|
|
|
209
273
|
}
|
|
210
274
|
runQuiet(command2, cwd, env);
|
|
211
275
|
}
|
|
212
|
-
|
|
213
|
-
|
|
276
|
+
var activeSpinner = null;
|
|
277
|
+
function renderSpinner(label, frame, startedAt) {
|
|
278
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
|
|
279
|
+
const elapsed = elapsedSeconds > 0 ? ` ${DIM}${elapsedSeconds}s${RESET}` : "";
|
|
280
|
+
process.stdout.write(`\r ${BLUE}${frame}${RESET} ${label}...${elapsed}\x1B[K`);
|
|
281
|
+
}
|
|
282
|
+
function stopSpinner(clearLine = true) {
|
|
283
|
+
if (!activeSpinner) return;
|
|
284
|
+
clearInterval(activeSpinner.interval);
|
|
285
|
+
if (clearLine) {
|
|
286
|
+
process.stdout.write("\r\x1B[K");
|
|
287
|
+
} else {
|
|
288
|
+
process.stdout.write("\n");
|
|
289
|
+
}
|
|
290
|
+
activeSpinner = null;
|
|
291
|
+
}
|
|
292
|
+
function stepStart(label, spinner = false) {
|
|
293
|
+
stopSpinner();
|
|
294
|
+
if (!spinner) {
|
|
295
|
+
blue(label);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
299
|
+
let frameIndex = 0;
|
|
300
|
+
const startedAt = Date.now();
|
|
301
|
+
renderSpinner(label, frames[frameIndex], startedAt);
|
|
302
|
+
const interval = setInterval(() => {
|
|
303
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
304
|
+
renderSpinner(label, frames[frameIndex], startedAt);
|
|
305
|
+
}, 80);
|
|
306
|
+
activeSpinner = {
|
|
307
|
+
interval,
|
|
308
|
+
label,
|
|
309
|
+
startedAt
|
|
310
|
+
};
|
|
214
311
|
}
|
|
215
312
|
function stepSuccess(label) {
|
|
313
|
+
stopSpinner();
|
|
216
314
|
green(label);
|
|
217
315
|
}
|
|
218
316
|
function stepFailure(label, errorLike, hint) {
|
|
317
|
+
stopSpinner();
|
|
219
318
|
error(`${label} failed.`);
|
|
220
319
|
const message = errorLike instanceof Error ? errorLike.message : String(errorLike);
|
|
221
320
|
const output = errorLike instanceof StepCommandError ? errorLike.output : getCommandOutput(errorLike);
|
|
@@ -232,6 +331,13 @@ function stepFailure(label, errorLike, hint) {
|
|
|
232
331
|
}
|
|
233
332
|
process.exit(1);
|
|
234
333
|
}
|
|
334
|
+
function attachSpinnerCleanup() {
|
|
335
|
+
const cleanup = () => stopSpinner();
|
|
336
|
+
process.on("exit", cleanup);
|
|
337
|
+
process.on("SIGINT", cleanup);
|
|
338
|
+
process.on("SIGTERM", cleanup);
|
|
339
|
+
}
|
|
340
|
+
attachSpinnerCleanup();
|
|
235
341
|
function openBrowser(url) {
|
|
236
342
|
const platform = process.platform;
|
|
237
343
|
const command2 = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
@@ -245,6 +351,101 @@ function writeJson(filePath, value) {
|
|
|
245
351
|
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
246
352
|
`);
|
|
247
353
|
}
|
|
354
|
+
function getShowpaneConfigPath() {
|
|
355
|
+
return join(SHOWPANE_HOME, "config.json");
|
|
356
|
+
}
|
|
357
|
+
function readShowpaneConfig() {
|
|
358
|
+
const configPath = getShowpaneConfigPath();
|
|
359
|
+
if (!existsSync(configPath)) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
return readJson(configPath);
|
|
363
|
+
}
|
|
364
|
+
function writeShowpaneConfig(config) {
|
|
365
|
+
ensureDir(SHOWPANE_HOME);
|
|
366
|
+
const configPath = getShowpaneConfigPath();
|
|
367
|
+
writeJson(configPath, config);
|
|
368
|
+
chmodSync(configPath, 384);
|
|
369
|
+
}
|
|
370
|
+
function findWorkspaceRoot(startPath) {
|
|
371
|
+
let currentPath = resolve(startPath);
|
|
372
|
+
while (true) {
|
|
373
|
+
if (existsSync(join(currentPath, "package.json")) && existsSync(join(currentPath, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(currentPath))) {
|
|
374
|
+
return currentPath;
|
|
375
|
+
}
|
|
376
|
+
const parentPath = dirname(currentPath);
|
|
377
|
+
if (parentPath === currentPath) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
currentPath = parentPath;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function defaultWorkspaceEntry(projectPath, overrides) {
|
|
384
|
+
return {
|
|
385
|
+
name: basename(projectPath),
|
|
386
|
+
path: resolve(projectPath),
|
|
387
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
deployMode: "local",
|
|
389
|
+
orgSlug: "",
|
|
390
|
+
...overrides
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function getWorkspaceEntries(config) {
|
|
394
|
+
const workspaces = [...config.workspaces ?? []];
|
|
395
|
+
const activePath = config.app_path ? resolve(config.app_path) : null;
|
|
396
|
+
if (activePath && !workspaces.some((workspace) => normalizePathForComparison(workspace.path) === normalizePathForComparison(activePath))) {
|
|
397
|
+
workspaces.push(defaultWorkspaceEntry(activePath, {
|
|
398
|
+
deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
|
|
399
|
+
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : ""
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return workspaces.map((workspace) => ({
|
|
403
|
+
...workspace,
|
|
404
|
+
path: resolve(workspace.path),
|
|
405
|
+
lastUsedAt: workspace.lastUsedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
406
|
+
deployMode: workspace.deployMode || "local",
|
|
407
|
+
orgSlug: workspace.orgSlug || ""
|
|
408
|
+
})).sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
409
|
+
}
|
|
410
|
+
function setActiveWorkspace(config, workspace) {
|
|
411
|
+
config.app_path = workspace.path;
|
|
412
|
+
config.deploy_mode = workspace.deployMode;
|
|
413
|
+
config.orgSlug = workspace.orgSlug;
|
|
414
|
+
}
|
|
415
|
+
function upsertWorkspace(config, workspace, makeActive = true) {
|
|
416
|
+
const workspaces = getWorkspaceEntries(config).filter(
|
|
417
|
+
(entry) => normalizePathForComparison(entry.path) !== normalizePathForComparison(workspace.path)
|
|
418
|
+
);
|
|
419
|
+
workspaces.push(workspace);
|
|
420
|
+
config.workspaces = workspaces.sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
421
|
+
if (makeActive) {
|
|
422
|
+
setActiveWorkspace(config, workspace);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function updateWorkspaceFromConfig(config, projectPath, overrides) {
|
|
426
|
+
const workspace = defaultWorkspaceEntry(projectPath, {
|
|
427
|
+
deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
|
|
428
|
+
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : "",
|
|
429
|
+
...overrides
|
|
430
|
+
});
|
|
431
|
+
upsertWorkspace(config, workspace, true);
|
|
432
|
+
return workspace;
|
|
433
|
+
}
|
|
434
|
+
function ensureShowpaneShim() {
|
|
435
|
+
ensureDir(SHOWPANE_BIN_DIR);
|
|
436
|
+
const shellShim = join(SHOWPANE_BIN_DIR, "showpane");
|
|
437
|
+
writeFileSync(
|
|
438
|
+
shellShim,
|
|
439
|
+
'#!/bin/sh\nexec npx --yes showpane "$@"\n'
|
|
440
|
+
);
|
|
441
|
+
chmodSync(shellShim, 493);
|
|
442
|
+
if (process.platform === "win32") {
|
|
443
|
+
writeFileSync(
|
|
444
|
+
join(SHOWPANE_BIN_DIR, "showpane.cmd"),
|
|
445
|
+
"@echo off\r\nnpx --yes showpane %*\r\n"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
248
449
|
function ensureDir(dirPath) {
|
|
249
450
|
mkdirSync(dirPath, { recursive: true });
|
|
250
451
|
}
|
|
@@ -581,7 +782,9 @@ function installSharedSkillProjection(toolchainRoot) {
|
|
|
581
782
|
process.platform === "win32" ? "junction" : "dir"
|
|
582
783
|
);
|
|
583
784
|
}
|
|
584
|
-
function printCreateSuccessCard(projectRoot,
|
|
785
|
+
function printCreateSuccessCard(projectRoot, url) {
|
|
786
|
+
const resumeCommand = getResumeCommand();
|
|
787
|
+
const resumeHint = getResumeHint();
|
|
585
788
|
console.log();
|
|
586
789
|
console.log(` ${GREEN}Showpane is ready${RESET}`);
|
|
587
790
|
console.log();
|
|
@@ -590,10 +793,14 @@ function printCreateSuccessCard(projectRoot, projectName, url) {
|
|
|
590
793
|
console.log(` ${BOLD}Demo:${RESET} example / demo-only-password`);
|
|
591
794
|
console.log();
|
|
592
795
|
console.log(` ${BOLD}Next:${RESET}`);
|
|
593
|
-
console.log(` ${DIM}
|
|
796
|
+
console.log(` ${DIM}${resumeCommand}${RESET}`);
|
|
594
797
|
console.log();
|
|
595
798
|
console.log(` ${BOLD}Try:${RESET}`);
|
|
596
799
|
console.log(` ${DIM}Create a portal for my call with Acme Health${RESET}`);
|
|
800
|
+
if (resumeHint) {
|
|
801
|
+
console.log();
|
|
802
|
+
console.log(` ${DIM}${resumeHint}${RESET}`);
|
|
803
|
+
}
|
|
597
804
|
console.log();
|
|
598
805
|
}
|
|
599
806
|
function startDevServer(projectRoot, databaseUrl, noOpen, verbose) {
|
|
@@ -650,6 +857,93 @@ function startDevServer(projectRoot, databaseUrl, noOpen, verbose) {
|
|
|
650
857
|
});
|
|
651
858
|
});
|
|
652
859
|
}
|
|
860
|
+
function resolveWorkspaceSelection(config, specifier) {
|
|
861
|
+
const workspaces = getWorkspaceEntries(config);
|
|
862
|
+
if (!specifier) {
|
|
863
|
+
return workspaces;
|
|
864
|
+
}
|
|
865
|
+
const asPathRoot = findWorkspaceRoot(specifier);
|
|
866
|
+
if (asPathRoot) {
|
|
867
|
+
return [defaultWorkspaceEntry(asPathRoot)];
|
|
868
|
+
}
|
|
869
|
+
const normalizedSpecifier = normalizePathForComparison(specifier);
|
|
870
|
+
const pathMatches = workspaces.filter(
|
|
871
|
+
(workspace) => normalizePathForComparison(workspace.path) === normalizedSpecifier
|
|
872
|
+
);
|
|
873
|
+
if (pathMatches.length > 0) {
|
|
874
|
+
return pathMatches;
|
|
875
|
+
}
|
|
876
|
+
const nameMatches = workspaces.filter((workspace) => workspace.name === specifier);
|
|
877
|
+
return nameMatches;
|
|
878
|
+
}
|
|
879
|
+
async function promptWorkspaceSelection(workspaces) {
|
|
880
|
+
console.log();
|
|
881
|
+
console.log(` ${BOLD}Select a Showpane workspace${RESET}`);
|
|
882
|
+
console.log();
|
|
883
|
+
for (const [index, workspace] of workspaces.entries()) {
|
|
884
|
+
console.log(` ${index + 1}. ${workspace.name}`);
|
|
885
|
+
console.log(` ${DIM}${workspace.path}${RESET}`);
|
|
886
|
+
console.log(` ${DIM}Last used: ${workspace.lastUsedAt}${RESET}`);
|
|
887
|
+
}
|
|
888
|
+
console.log();
|
|
889
|
+
while (true) {
|
|
890
|
+
const answer = await ask(` ${BOLD}Choose a workspace [1-${workspaces.length}] or q to cancel:${RESET} `);
|
|
891
|
+
if (!answer) continue;
|
|
892
|
+
if (answer.toLowerCase() === "q") {
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
const selectedIndex = Number.parseInt(answer, 10);
|
|
896
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= workspaces.length) {
|
|
897
|
+
return workspaces[selectedIndex - 1];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function printWorkspaceList(config) {
|
|
902
|
+
printBanner();
|
|
903
|
+
const workspaces = getWorkspaceEntries(config);
|
|
904
|
+
if (workspaces.length === 0) {
|
|
905
|
+
console.log();
|
|
906
|
+
blue("No Showpane workspaces found");
|
|
907
|
+
console.log(` ${DIM}Run: npx showpane${RESET}`);
|
|
908
|
+
console.log();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const activePath = config.app_path ? normalizePathForComparison(config.app_path) : null;
|
|
912
|
+
console.log();
|
|
913
|
+
console.log(` ${BOLD}Showpane workspaces${RESET}`);
|
|
914
|
+
console.log();
|
|
915
|
+
for (const [index, workspace] of workspaces.entries()) {
|
|
916
|
+
const isActive = activePath !== null && normalizePathForComparison(workspace.path) === activePath;
|
|
917
|
+
const marker = isActive ? "*" : " ";
|
|
918
|
+
console.log(` ${marker} ${index + 1}. ${workspace.name}`);
|
|
919
|
+
console.log(` ${workspace.path}`);
|
|
920
|
+
console.log(` ${DIM}Last used: ${workspace.lastUsedAt}${RESET}`);
|
|
921
|
+
}
|
|
922
|
+
console.log();
|
|
923
|
+
}
|
|
924
|
+
async function openClaudeInWorkspace(workspace) {
|
|
925
|
+
if (!commandExists("claude")) {
|
|
926
|
+
throw new Error("Claude Code is not installed or not on PATH.");
|
|
927
|
+
}
|
|
928
|
+
blue(`Opening ${workspace.name} workspace`);
|
|
929
|
+
console.log(` ${DIM}${workspace.path}${RESET}`);
|
|
930
|
+
console.log();
|
|
931
|
+
await new Promise((resolveLaunch, rejectLaunch) => {
|
|
932
|
+
const child = spawn("claude", [], {
|
|
933
|
+
cwd: workspace.path,
|
|
934
|
+
stdio: "inherit",
|
|
935
|
+
env: {
|
|
936
|
+
...process.env,
|
|
937
|
+
SHOWPANE_APP_PATH: workspace.path,
|
|
938
|
+
SHOWPANE_TOOLCHAIN_DIR: CURRENT_TOOLCHAIN_LINK
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
child.on("error", rejectLaunch);
|
|
942
|
+
child.on("close", (code) => {
|
|
943
|
+
process.exit(code ?? 0);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
653
947
|
function installSkillProjection(toolchainRoot) {
|
|
654
948
|
removePath(join(CLAUDE_SKILLS_DIR, "showpane"));
|
|
655
949
|
installSharedSkillProjection(toolchainRoot);
|
|
@@ -736,6 +1030,7 @@ async function createProject(args) {
|
|
|
736
1030
|
process.exit(1);
|
|
737
1031
|
}
|
|
738
1032
|
printBanner();
|
|
1033
|
+
ensureShowpaneShim();
|
|
739
1034
|
const companyName = options.companyName ?? await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
740
1035
|
if (!companyName) {
|
|
741
1036
|
error("Company name is required.");
|
|
@@ -793,6 +1088,13 @@ AUTH_SECRET="${authSecret}"
|
|
|
793
1088
|
let toolchainInfo;
|
|
794
1089
|
try {
|
|
795
1090
|
toolchainInfo = syncToolchain(bundleRoot, showpaneVersion, false);
|
|
1091
|
+
const config = readShowpaneConfig();
|
|
1092
|
+
updateWorkspaceFromConfig(config, projectRoot, {
|
|
1093
|
+
name: dirName,
|
|
1094
|
+
deployMode: "local",
|
|
1095
|
+
orgSlug: ""
|
|
1096
|
+
});
|
|
1097
|
+
writeShowpaneConfig(config);
|
|
796
1098
|
writeProjectState(
|
|
797
1099
|
projectRoot,
|
|
798
1100
|
showpaneVersion,
|
|
@@ -824,7 +1126,7 @@ AUTH_SECRET="${authSecret}"
|
|
|
824
1126
|
`Run ${BOLD}cd ${dirName} && npm run dev${RESET} for more detail.`
|
|
825
1127
|
);
|
|
826
1128
|
}
|
|
827
|
-
printCreateSuccessCard(projectRoot,
|
|
1129
|
+
printCreateSuccessCard(projectRoot, serverStart.url);
|
|
828
1130
|
serverStart.devServer.on("close", (code) => {
|
|
829
1131
|
if (code !== 0) {
|
|
830
1132
|
error(`Dev server exited with code ${code}`);
|
|
@@ -842,6 +1144,7 @@ async function syncCurrentToolchain() {
|
|
|
842
1144
|
const packageRoot2 = getPackageRoot();
|
|
843
1145
|
const bundleRoot = getLocalBundleRoot(packageRoot2);
|
|
844
1146
|
const showpaneVersion = getPackageVersion(packageRoot2);
|
|
1147
|
+
ensureShowpaneShim();
|
|
845
1148
|
printBanner();
|
|
846
1149
|
maybePrintShowpaneUpdateMessage(showpaneVersion);
|
|
847
1150
|
console.log();
|
|
@@ -914,8 +1217,73 @@ async function upgradeProject(args) {
|
|
|
914
1217
|
bundleSource.cleanup();
|
|
915
1218
|
}
|
|
916
1219
|
}
|
|
1220
|
+
async function openClaude(args) {
|
|
1221
|
+
let options;
|
|
1222
|
+
try {
|
|
1223
|
+
options = parseClaudeArgs(args);
|
|
1224
|
+
} catch (errorLike) {
|
|
1225
|
+
printBanner();
|
|
1226
|
+
console.log();
|
|
1227
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
1228
|
+
printClaudeUsage();
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
ensureShowpaneShim();
|
|
1232
|
+
const config = readShowpaneConfig();
|
|
1233
|
+
const workspaces = getWorkspaceEntries(config);
|
|
1234
|
+
if (workspaces.length === 0 && !options.project) {
|
|
1235
|
+
blue("No Showpane workspace found. Let's create one first.");
|
|
1236
|
+
console.log();
|
|
1237
|
+
await createProject(args);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (workspaces.length > 0 && (options.companyName || options.yes)) {
|
|
1241
|
+
error("`--yes` and `--name` only apply when creating the first workspace.");
|
|
1242
|
+
printClaudeUsage();
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
let workspace;
|
|
1246
|
+
if (options.project) {
|
|
1247
|
+
const matches = resolveWorkspaceSelection(config, options.project);
|
|
1248
|
+
if (matches.length === 0) {
|
|
1249
|
+
error(`Could not find a Showpane workspace matching: ${options.project}`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
if (matches.length > 1) {
|
|
1253
|
+
error(`Multiple workspaces matched: ${options.project}`);
|
|
1254
|
+
for (const match of matches) {
|
|
1255
|
+
console.error(` ${match.name} \u2014 ${match.path}`);
|
|
1256
|
+
}
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
workspace = matches[0];
|
|
1260
|
+
} else if (workspaces.length === 1) {
|
|
1261
|
+
workspace = workspaces[0];
|
|
1262
|
+
} else if (!process.stdout.isTTY) {
|
|
1263
|
+
error("Multiple Showpane workspaces found. Use `showpane claude --project <name-or-path>`.");
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
} else {
|
|
1266
|
+
workspace = await promptWorkspaceSelection(workspaces);
|
|
1267
|
+
}
|
|
1268
|
+
const workspaceRoot = findWorkspaceRoot(workspace.path) ?? resolve(workspace.path);
|
|
1269
|
+
const selectedWorkspace = defaultWorkspaceEntry(workspaceRoot, {
|
|
1270
|
+
name: workspace.name || basename(workspaceRoot),
|
|
1271
|
+
deployMode: workspace.deployMode || "local",
|
|
1272
|
+
orgSlug: workspace.orgSlug || ""
|
|
1273
|
+
});
|
|
1274
|
+
upsertWorkspace(config, selectedWorkspace, true);
|
|
1275
|
+
writeShowpaneConfig(config);
|
|
1276
|
+
try {
|
|
1277
|
+
await openClaudeInWorkspace(selectedWorkspace);
|
|
1278
|
+
} catch (errorLike) {
|
|
1279
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
1280
|
+
console.error(`Hint: Install Claude Code first, or use ${getResumeCommand()} later.`);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
917
1284
|
async function login() {
|
|
918
1285
|
printBanner();
|
|
1286
|
+
ensureShowpaneShim();
|
|
919
1287
|
blue("Authenticating with Showpane...");
|
|
920
1288
|
console.log();
|
|
921
1289
|
const initRes = await fetch(`${API_BASE}/api/cli/init`, { method: "POST" });
|
|
@@ -944,26 +1312,23 @@ async function login() {
|
|
|
944
1312
|
}
|
|
945
1313
|
const data = await pollRes.json();
|
|
946
1314
|
if (data.status !== "approved") continue;
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
)
|
|
965
|
-
);
|
|
966
|
-
chmodSync(configPath, 384);
|
|
1315
|
+
const config = readShowpaneConfig();
|
|
1316
|
+
config.accessToken = data.accessToken;
|
|
1317
|
+
config.accessTokenExpiresAt = data.tokenExpiresAt;
|
|
1318
|
+
config.orgSlug = data.orgSlug;
|
|
1319
|
+
config.portalUrl = data.portalUrl;
|
|
1320
|
+
config.vercelProjectId = data.vercelProjectId;
|
|
1321
|
+
const currentWorkspace = findWorkspaceRoot(process.cwd()) ?? (config.app_path ? findWorkspaceRoot(config.app_path) ?? resolve(config.app_path) : null);
|
|
1322
|
+
if (currentWorkspace) {
|
|
1323
|
+
updateWorkspaceFromConfig(config, currentWorkspace, {
|
|
1324
|
+
name: basename(currentWorkspace),
|
|
1325
|
+
deployMode: "cloud",
|
|
1326
|
+
orgSlug: data.orgSlug
|
|
1327
|
+
});
|
|
1328
|
+
} else {
|
|
1329
|
+
config.deploy_mode = "cloud";
|
|
1330
|
+
}
|
|
1331
|
+
writeShowpaneConfig(config);
|
|
967
1332
|
console.log();
|
|
968
1333
|
green(`Authenticated! Connected to ${BOLD}${data.orgSlug}${RESET}`);
|
|
969
1334
|
console.log();
|
|
@@ -983,6 +1348,18 @@ if (command === "login") {
|
|
|
983
1348
|
error(String(err));
|
|
984
1349
|
process.exit(1);
|
|
985
1350
|
});
|
|
1351
|
+
} else if (command === "claude") {
|
|
1352
|
+
openClaude(process.argv.slice(3)).catch((err) => {
|
|
1353
|
+
error(String(err));
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
});
|
|
1356
|
+
} else if (command === "projects") {
|
|
1357
|
+
try {
|
|
1358
|
+
printWorkspaceList(readShowpaneConfig());
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
error(String(err));
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
986
1363
|
} else if (command === "sync") {
|
|
987
1364
|
syncCurrentToolchain().catch((err) => {
|
|
988
1365
|
error(String(err));
|