showpane 0.0.1 → 0.2.1
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 +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +264 -0
- package/package.json +35 -7
- package/index.js +0 -3
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# showpane
|
|
2
|
+
|
|
3
|
+
CLI for [Showpane](https://showpane.com) — AI-generated client portals.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx showpane
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### `npx showpane` (default)
|
|
14
|
+
Scaffold a new Showpane portal project. Sets up Next.js, Prisma, and example portal.
|
|
15
|
+
|
|
16
|
+
### `showpane login`
|
|
17
|
+
Authenticate with Showpane Cloud for portal deployment.
|
|
18
|
+
|
|
19
|
+
## Requirements
|
|
20
|
+
|
|
21
|
+
- Node.js 20+
|
|
22
|
+
- Claude Code (for portal creation)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
import { execSync, spawn, exec } from "child_process";
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import { randomBytes } from "crypto";
|
|
8
|
+
import { createServer } from "net";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import os from "os";
|
|
11
|
+
import { fileURLToPath } from "url";
|
|
12
|
+
var { mkdirSync, readFileSync, writeFileSync } = fs;
|
|
13
|
+
var { resolve, dirname, join } = path;
|
|
14
|
+
var { homedir } = os;
|
|
15
|
+
var RESET = "\x1B[0m";
|
|
16
|
+
var BOLD = "\x1B[1m";
|
|
17
|
+
var DIM = "\x1B[2m";
|
|
18
|
+
var GREEN = "\x1B[32m";
|
|
19
|
+
var BLUE = "\x1B[34m";
|
|
20
|
+
var WHITE = "\x1B[37m";
|
|
21
|
+
var RED = "\x1B[31m";
|
|
22
|
+
function green(msg) {
|
|
23
|
+
console.log(` ${GREEN}\u2713${RESET} ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
function blue(msg) {
|
|
26
|
+
console.log(` ${BLUE}\u2192${RESET} ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
function error(msg) {
|
|
29
|
+
console.error(` ${RED}\u2717${RESET} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
function printBanner() {
|
|
32
|
+
const banner = `
|
|
33
|
+
${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
|
|
34
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D
|
|
35
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557
|
|
36
|
+
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
|
|
37
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
38
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}
|
|
39
|
+
${DIM} Client portals that close deals.${RESET}
|
|
40
|
+
`;
|
|
41
|
+
console.log(banner);
|
|
42
|
+
}
|
|
43
|
+
function ask(question) {
|
|
44
|
+
const rl = createInterface({
|
|
45
|
+
input: process.stdin,
|
|
46
|
+
output: process.stdout
|
|
47
|
+
});
|
|
48
|
+
return new Promise((resolve2) => {
|
|
49
|
+
rl.question(question, (answer) => {
|
|
50
|
+
rl.close();
|
|
51
|
+
resolve2(answer.trim());
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function toSlug(name) {
|
|
56
|
+
return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
57
|
+
}
|
|
58
|
+
function findFreePort(startPort) {
|
|
59
|
+
return new Promise((resolve2, reject) => {
|
|
60
|
+
const server = createServer();
|
|
61
|
+
server.listen(startPort, () => {
|
|
62
|
+
server.close(() => resolve2(startPort));
|
|
63
|
+
});
|
|
64
|
+
server.on("error", (err) => {
|
|
65
|
+
if (err.code === "EADDRINUSE") {
|
|
66
|
+
resolve2(findFreePort(startPort + 1));
|
|
67
|
+
} else {
|
|
68
|
+
reject(err);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function run(cmd, cwd) {
|
|
74
|
+
execSync(cmd, { cwd, stdio: "inherit" });
|
|
75
|
+
}
|
|
76
|
+
function openBrowser(url) {
|
|
77
|
+
const platform = process.platform;
|
|
78
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
79
|
+
exec(`${cmd} ${url}`);
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
if (process.argv.includes("--version")) {
|
|
83
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
84
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
85
|
+
console.log(pkg.version);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
printBanner();
|
|
89
|
+
const companyName = await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
90
|
+
if (!companyName) {
|
|
91
|
+
error("Company name is required.");
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
const slug = toSlug(companyName);
|
|
95
|
+
const dirName = `showpane-${slug}`;
|
|
96
|
+
console.log();
|
|
97
|
+
blue(`Setting up ${BOLD}${companyName}${RESET} portal as ${DIM}${dirName}/${RESET}`);
|
|
98
|
+
console.log();
|
|
99
|
+
try {
|
|
100
|
+
run(
|
|
101
|
+
`git clone --depth 1 https://github.com/twillcocks/showpane.git ${dirName}`
|
|
102
|
+
);
|
|
103
|
+
green("Cloned repository");
|
|
104
|
+
} catch {
|
|
105
|
+
error("Failed to clone repository. Check your internet connection and try again.");
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const appDir = resolve(process.cwd(), dirName, "app");
|
|
109
|
+
try {
|
|
110
|
+
run("npm install", appDir);
|
|
111
|
+
green("Dependencies installed");
|
|
112
|
+
} catch {
|
|
113
|
+
error("Failed to install dependencies.");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const authSecret = randomBytes(32).toString("hex");
|
|
117
|
+
const envContent = `DATABASE_URL="file:./dev.db"
|
|
118
|
+
AUTH_SECRET="${authSecret}"
|
|
119
|
+
`;
|
|
120
|
+
writeFileSync(resolve(appDir, ".env"), envContent);
|
|
121
|
+
green("Environment configured");
|
|
122
|
+
try {
|
|
123
|
+
run("npx prisma db push --schema prisma/schema.local.prisma", appDir);
|
|
124
|
+
green("Database ready");
|
|
125
|
+
} catch {
|
|
126
|
+
error("Failed to set up the database. Check Prisma schema and try again.");
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
run("npx tsx prisma/seed.ts", appDir);
|
|
131
|
+
green("Example portal seeded");
|
|
132
|
+
} catch {
|
|
133
|
+
blue("Skipped example portal (seed failed \u2014 not a problem)");
|
|
134
|
+
}
|
|
135
|
+
const skillsSource = path.join(dirName, "skills");
|
|
136
|
+
const skillsTarget = path.join(os.homedir(), ".claude", "skills");
|
|
137
|
+
const oldSymlink = path.join(skillsTarget, "showpane");
|
|
138
|
+
try {
|
|
139
|
+
const stat = fs.lstatSync(oldSymlink);
|
|
140
|
+
if (stat.isSymbolicLink()) fs.unlinkSync(oldSymlink);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
fs.mkdirSync(skillsTarget, { recursive: true });
|
|
144
|
+
const skillDirs = fs.readdirSync(skillsSource, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("portal-"));
|
|
145
|
+
for (const dir of skillDirs) {
|
|
146
|
+
const targetDir = path.join(skillsTarget, dir.name);
|
|
147
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
148
|
+
const skillFile = path.join(targetDir, "SKILL.md");
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(skillFile);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
fs.symlinkSync(
|
|
154
|
+
path.join(skillsSource, dir.name, "SKILL.md"),
|
|
155
|
+
skillFile
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
green(`${skillDirs.length} Claude Code skills installed`);
|
|
159
|
+
const port = await findFreePort(3e3);
|
|
160
|
+
green(`Server starting on port ${port}`);
|
|
161
|
+
const url = `http://localhost:${port}`;
|
|
162
|
+
blue(`Opening ${url}`);
|
|
163
|
+
console.log();
|
|
164
|
+
console.log(` ${GREEN}Ready!${RESET} ${skillDirs.length} Claude Code skills installed.`);
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(` Open Claude Code and create your first portal:`);
|
|
167
|
+
console.log(` ${DIM}cd ${dirName}/app${RESET}`);
|
|
168
|
+
console.log(` ${BOLD}claude${RESET}`);
|
|
169
|
+
console.log(` ${DIM}> Create a portal for my call with [client name]${RESET}`);
|
|
170
|
+
console.log();
|
|
171
|
+
console.log(` Available skills: /portal-create, /portal-deploy, /portal-list, ...`);
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(` ${DIM}Don't have Claude Code? Install from https://claude.ai/code${RESET}`);
|
|
174
|
+
console.log();
|
|
175
|
+
const devServer = spawn("npm", ["run", "dev"], {
|
|
176
|
+
cwd: appDir,
|
|
177
|
+
stdio: "inherit",
|
|
178
|
+
env: { ...process.env, PORT: String(port) }
|
|
179
|
+
});
|
|
180
|
+
setTimeout(() => {
|
|
181
|
+
openBrowser(url);
|
|
182
|
+
}, 3e3);
|
|
183
|
+
devServer.on("close", (code) => {
|
|
184
|
+
if (code !== 0) {
|
|
185
|
+
error(`Dev server exited with code ${code}`);
|
|
186
|
+
}
|
|
187
|
+
process.exit(code ?? 1);
|
|
188
|
+
});
|
|
189
|
+
process.on("SIGINT", () => {
|
|
190
|
+
devServer.kill("SIGINT");
|
|
191
|
+
});
|
|
192
|
+
process.on("SIGTERM", () => {
|
|
193
|
+
devServer.kill("SIGTERM");
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
var API_BASE = "https://app.showpane.com";
|
|
197
|
+
async function login() {
|
|
198
|
+
printBanner();
|
|
199
|
+
blue("Authenticating with Showpane...");
|
|
200
|
+
console.log();
|
|
201
|
+
const initRes = await fetch(`${API_BASE}/api/cli/init`, { method: "POST" });
|
|
202
|
+
if (!initRes.ok) {
|
|
203
|
+
throw new Error(`Failed to start auth flow (${initRes.status})`);
|
|
204
|
+
}
|
|
205
|
+
const { code, userCode, verificationUrl } = await initRes.json();
|
|
206
|
+
console.log(` ${BOLD}Enter this code in your browser:${RESET} ${BOLD}${GREEN}${userCode}${RESET}`);
|
|
207
|
+
console.log();
|
|
208
|
+
openBrowser(verificationUrl);
|
|
209
|
+
blue(`Opened ${verificationUrl}`);
|
|
210
|
+
console.log();
|
|
211
|
+
blue("Waiting for authorization...");
|
|
212
|
+
const POLL_INTERVAL = 2e3;
|
|
213
|
+
const TIMEOUT = 10 * 60 * 1e3;
|
|
214
|
+
const start = Date.now();
|
|
215
|
+
while (Date.now() - start < TIMEOUT) {
|
|
216
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
217
|
+
const pollRes = await fetch(`${API_BASE}/api/cli/poll?code=${code}`);
|
|
218
|
+
if (pollRes.status === 410) {
|
|
219
|
+
error("Code expired. Please try again.");
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
if (!pollRes.ok && pollRes.status !== 202) {
|
|
223
|
+
throw new Error(`Poll failed (${pollRes.status})`);
|
|
224
|
+
}
|
|
225
|
+
const data = await pollRes.json();
|
|
226
|
+
if (data.status === "approved") {
|
|
227
|
+
const configDir = join(homedir(), ".showpane");
|
|
228
|
+
mkdirSync(configDir, { recursive: true });
|
|
229
|
+
const configPath = join(configDir, "config.json");
|
|
230
|
+
writeFileSync(
|
|
231
|
+
configPath,
|
|
232
|
+
JSON.stringify(
|
|
233
|
+
{
|
|
234
|
+
accessToken: data.accessToken,
|
|
235
|
+
orgSlug: data.orgSlug,
|
|
236
|
+
portalUrl: data.portalUrl,
|
|
237
|
+
vercelProjectId: data.vercelProjectId,
|
|
238
|
+
app_path: join(process.cwd(), "app"),
|
|
239
|
+
deploy_mode: "cloud"
|
|
240
|
+
},
|
|
241
|
+
null,
|
|
242
|
+
2
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
console.log();
|
|
246
|
+
green(`Authenticated! Connected to ${BOLD}${data.orgSlug}${RESET}`);
|
|
247
|
+
console.log();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
error("Authentication timed out. Please try again.");
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
if (process.argv[2] === "login") {
|
|
255
|
+
login().catch((err) => {
|
|
256
|
+
error(String(err));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
} else {
|
|
260
|
+
main().catch((err) => {
|
|
261
|
+
error(String(err));
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "showpane",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "CLI for Showpane — AI-generated client portals",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"bin": {
|
|
6
|
-
"showpane": "./index.js"
|
|
7
|
+
"showpane": "./dist/index.js"
|
|
7
8
|
},
|
|
8
|
-
"
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
16
|
+
"dev": "tsx src/index.ts",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"showpane",
|
|
21
|
+
"client-portal",
|
|
22
|
+
"claude-code",
|
|
23
|
+
"portal",
|
|
24
|
+
"scaffolding",
|
|
25
|
+
"cli"
|
|
26
|
+
],
|
|
9
27
|
"author": "twillcocks",
|
|
10
|
-
"license": "
|
|
28
|
+
"license": "AGPL-3.0",
|
|
11
29
|
"repository": {
|
|
12
30
|
"type": "git",
|
|
13
|
-
"url": "https://github.com/twillcocks/showpane"
|
|
31
|
+
"url": "https://github.com/twillcocks/showpane.git",
|
|
32
|
+
"directory": "packages/cli"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://showpane.com",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20"
|
|
14
37
|
},
|
|
15
|
-
"
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^25.5.2",
|
|
40
|
+
"tsup": "^8.4.0",
|
|
41
|
+
"tsx": "^4.19.4",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
16
44
|
}
|
package/index.js
DELETED