showpane 0.4.13 → 0.4.15
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 +2 -1
- package/bundle/meta/scaffold-manifest.json +10 -10
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/prisma/seed.ts +40 -35
- package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +7 -0
- package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +1 -2
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +5 -4
- package/bundle/scaffold/src/app/page.tsx +43 -6
- package/bundle/scaffold/src/components/portal-shell.tsx +23 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
- package/bundle/toolchain/CLI_VERSION +1 -0
- package/bundle/toolchain/TELEMETRY_CONFIG.json +4 -0
- package/bundle/toolchain/VERSION +1 -1
- package/bundle/toolchain/bin/ensure-cloud-project-link.ts +34 -1
- package/bundle/toolchain/bin/showpane-config +108 -29
- package/bundle/toolchain/bin/showpane-telemetry-log +84 -0
- package/bundle/toolchain/bin/showpane-telemetry-sync +212 -0
- package/bundle/toolchain/bin/showpane-update-check +130 -0
- package/bundle/toolchain/skills/SKILL.md.tmpl +13 -0
- package/bundle/toolchain/skills/VERSION +1 -1
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +60 -38
- package/bundle/toolchain/skills/portal-analytics/SKILL.md.tmpl +192 -0
- package/bundle/toolchain/skills/portal-create/SKILL.md +65 -67
- package/bundle/toolchain/skills/portal-create/SKILL.md.tmpl +264 -0
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +66 -49
- package/bundle/toolchain/skills/portal-credentials/SKILL.md.tmpl +198 -0
- package/bundle/toolchain/skills/portal-delete/SKILL.md +63 -41
- package/bundle/toolchain/skills/portal-delete/SKILL.md.tmpl +194 -0
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +57 -47
- package/bundle/toolchain/skills/portal-deploy/SKILL.md.tmpl +452 -0
- package/bundle/toolchain/skills/portal-dev/SKILL.md +65 -47
- package/bundle/toolchain/skills/portal-dev/SKILL.md.tmpl +228 -0
- package/bundle/toolchain/skills/portal-list/SKILL.md +64 -43
- package/bundle/toolchain/skills/portal-list/SKILL.md.tmpl +181 -0
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +331 -162
- package/bundle/toolchain/skills/portal-onboard/SKILL.md.tmpl +340 -0
- package/bundle/toolchain/skills/portal-preview/SKILL.md +65 -44
- package/bundle/toolchain/skills/portal-preview/SKILL.md.tmpl +171 -0
- package/bundle/toolchain/skills/portal-setup/SKILL.md +79 -60
- package/bundle/toolchain/skills/portal-setup/SKILL.md.tmpl +227 -0
- package/bundle/toolchain/skills/portal-share/SKILL.md +69 -47
- package/bundle/toolchain/skills/portal-share/SKILL.md.tmpl +162 -0
- package/bundle/toolchain/skills/portal-status/SKILL.md +58 -37
- package/bundle/toolchain/skills/portal-status/SKILL.md.tmpl +196 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md +60 -46
- package/bundle/toolchain/skills/portal-update/SKILL.md.tmpl +269 -0
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +55 -33
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md.tmpl +164 -0
- package/bundle/toolchain/skills/portal-verify/SKILL.md +69 -14
- package/bundle/toolchain/skills/portal-verify/SKILL.md.tmpl +224 -0
- package/bundle/toolchain/skills/shared/preamble.md +30 -126
- package/bundle/toolchain/skills/shared/runtime-principles.md +25 -0
- package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +1 -1
- package/dist/index.js +79 -14
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ Flags:
|
|
|
22
22
|
|
|
23
23
|
### `showpane login`
|
|
24
24
|
Authenticate with Showpane Cloud for hosted portal deployment.
|
|
25
|
-
This is auth only
|
|
25
|
+
This is auth only. If the workspace is not billing/provisioning-ready yet, the
|
|
26
|
+
cloud flow will send you to the relevant Showpane Cloud checkout or settings step.
|
|
26
27
|
|
|
27
28
|
### `showpane claude`
|
|
28
29
|
Resume your Showpane workspace by launching Claude Code in the right project directory.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-
|
|
4
|
-
"scaffoldVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-04-11T21:50:01.081Z",
|
|
4
|
+
"scaffoldVersion": "0.2.6",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
|
|
7
7
|
".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
|
|
8
|
-
"VERSION": "
|
|
8
|
+
"VERSION": "be3c6d2c6c406a64d44f0b6464a887e290416dd90c524094485b1be00936d6d7",
|
|
9
9
|
"next.config.ts": "cf27999cc274cce79bc4c8df11789807719abf40752b60e4b4967a3d2f0ed013",
|
|
10
10
|
"package-lock.json": "d8e30eb86f08e70787d4459a084b4ab2a9f119696bbd3146ec4ba5675fffd3c2",
|
|
11
11
|
"package.json": "b095e17e7fc181c630e87fe9f473c5a4ef969afcd4b110f9f9c6d6a6d93f1c0b",
|
|
12
12
|
"postcss.config.js": "fa650b380adfabb151a0b352f7135e107e6352345f899060f1c5c231228f94bf",
|
|
13
13
|
"prisma.config.ts": "36f56fd74eae70632e484443e38d08665158d72d5c978dc456651d8d5e1a636e",
|
|
14
14
|
"prisma/schema.local.prisma": "f5d6f3cb17d6d229f46ef82eef7c0ff4261596924f0173fef075ac394f423073",
|
|
15
|
-
"prisma/seed.ts": "
|
|
15
|
+
"prisma/seed.ts": "bb8eb3bc189fabbab3be83d3909afee583a5b724d258139feda22af504bed134",
|
|
16
16
|
"public/example-avatar.svg": "0edeb0d3fbefa89cc27ffe6564d20e3ee0fd073cb6d9f2a025248ef3b3f277fd",
|
|
17
17
|
"public/example-logo.svg": "bc5cd933aff2a17698dee66a7b4ea940ad12238e9d813474d643b459b1e8d6da",
|
|
18
18
|
"public/robots.txt": "331ea9090db0c9f6f597bd9840fd5b171830f6e0b3ba1cb24dfa91f0c95aedc1",
|
|
@@ -21,13 +21,13 @@
|
|
|
21
21
|
"scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
|
|
22
22
|
"src/__tests__/client-portals.test.ts": "9c3236bf0f7190b7d5ba9082287dcb29bc00d28dd63782a89505125ead06c624",
|
|
23
23
|
"src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
|
|
24
|
-
"src/__tests__/portal-contracts.test.ts": "
|
|
24
|
+
"src/__tests__/portal-contracts.test.ts": "9562b67f84f528ee0bb0bc4c0225adaf1dcc38c59b7629bda7c44e374a020eac",
|
|
25
25
|
"src/app/(portal)/client/[slug]/page.tsx": "4f2f9253b2ad5d37a0f13759db52c786ae9c401f50fae9431da1417e9736e000",
|
|
26
26
|
"src/app/(portal)/client/[slug]/s/[token]/route.ts": "a445e54b9139e40dfe0bc039e34e6af224a27f75a614741ab224d317ad4d3ec9",
|
|
27
|
-
"src/app/(portal)/client/example/example-client.tsx": "
|
|
27
|
+
"src/app/(portal)/client/example/example-client.tsx": "ed32b111acea861f448d865338f8841d47c6ca7c2f87ed30d85bb0804940d4ec",
|
|
28
28
|
"src/app/(portal)/client/example/page.tsx": "f330864f63c9feea76c8a62c3eba3ce57578627e0d4abd929fd7fefdfc7af058",
|
|
29
29
|
"src/app/(portal)/client/layout.tsx": "4f43871510408a81da229d48ae316ec1d1c1beda93121922246300a2c8fd0999",
|
|
30
|
-
"src/app/(portal)/client/page.tsx": "
|
|
30
|
+
"src/app/(portal)/client/page.tsx": "7b4e3e5b286672f83581e24349e98ac930d8bbf5dfed02793754be2ddadf430f",
|
|
31
31
|
"src/app/api/client-auth/route.ts": "ce1858559b1e944d5b1dc719d1f03bebf66286671700b1b5397382109f0f1e0d",
|
|
32
32
|
"src/app/api/client-auth/share/route.ts": "ed82414212dcd26af8c6c0f2bd44d9d79a727ed35cfedbac8c4077a6220aad14",
|
|
33
33
|
"src/app/api/client-events/route.ts": "13d545537b7e8ce421e6169d25c105adf2a2de3d978ae0a2c6751ff5f7d2eb33",
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
"src/app/api/health/route.ts": "78fff55707372ce0cd6e9e49ef4f049622bc43cc42916d3f83e0162409d678b1",
|
|
39
39
|
"src/app/globals.css": "28dcda76006d0e6af01b6dcf1a315dc5b5b6931c880fc53fd6565ff09d5dd13a",
|
|
40
40
|
"src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
|
|
41
|
-
"src/app/page.tsx": "
|
|
41
|
+
"src/app/page.tsx": "1f71205c3ae30bf6929d37947b0c94ae53aed33b6689b7d6b13066ad51c1bc14",
|
|
42
42
|
"src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
|
|
43
43
|
"src/components/portal-login.tsx": "8b0d91bb28674e1102fd2e5b5ddcc3a93755dd806fbd3d1b2dbea2646cffca5e",
|
|
44
|
-
"src/components/portal-shell.tsx": "
|
|
44
|
+
"src/components/portal-shell.tsx": "f46a0f753a4a0318f06c8b4e46295febb84b03ea082c95057a6da50c737b4e21",
|
|
45
45
|
"src/lib/abuse-controls.ts": "d79d58d93267aca48ad0b7b9b91f753c9a3c27263e4e98daf768a950c44a6fc6",
|
|
46
46
|
"src/lib/branding.ts": "cc55f40e02bc3e486b227988f95739ca1cda8012c97b591295995eb4465efd57",
|
|
47
47
|
"src/lib/client-auth.ts": "b9bdfe77dbe5d6ec6c6a930627fc43d3253f0d76fd8fc4093af5a75742bebe42",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"src/lib/deploy-bundle.ts": "e9675cccb2c802e408639481986c6b629737541853e1c93f322c08a5b9dfc5f9",
|
|
52
52
|
"src/lib/files.ts": "24fd8d1d53c180d62441019395fb140ba3baa28311918ac488284adcdda8eb9a",
|
|
53
53
|
"src/lib/load-app-env.ts": "78b80e17d896885f0d72315ee9a6cf7a0a8c6c08171f26e3d599bb9b2e8afeee",
|
|
54
|
-
"src/lib/portal-contracts.ts": "
|
|
54
|
+
"src/lib/portal-contracts.ts": "7954e5c66ebc2159d77d0b4c3a5f0d0a561ab020de8cefb0ddd52c7688039108",
|
|
55
55
|
"src/lib/prisma-client.ts": "28cd100129a0178a6c8fdfe49e6997b19983fcc427b9fa7caee3ac26226e5eb3",
|
|
56
56
|
"src/lib/runtime-state.ts": "3d30de7dfeaaa48d8b6fd5d29976ecd001408172100c95b063d5d804fdce0a2e",
|
|
57
57
|
"src/lib/storage.ts": "ae3b85fc6cccd39d4174a391dcbe6e91fb9460eb407ec9dbfedd63594a441d08",
|
package/bundle/scaffold/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.6
|
|
@@ -1,49 +1,54 @@
|
|
|
1
1
|
import { PrismaClient } from "@/lib/prisma-client";
|
|
2
|
-
import bcrypt from "bcryptjs";
|
|
3
2
|
|
|
4
3
|
const prisma = new PrismaClient();
|
|
5
4
|
|
|
5
|
+
function normalizeSlug(value: string): string {
|
|
6
|
+
return value
|
|
7
|
+
.toLowerCase()
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
10
|
+
.replace(/\s+/g, "-")
|
|
11
|
+
.replace(/-+/g, "-")
|
|
12
|
+
.replace(/^-|-$/g, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeWebsiteUrl(value: string | undefined): string | null {
|
|
16
|
+
if (!value) return null;
|
|
17
|
+
const trimmed = value.trim();
|
|
18
|
+
if (!trimmed) return null;
|
|
19
|
+
return trimmed.startsWith("http://") || trimmed.startsWith("https://")
|
|
20
|
+
? trimmed
|
|
21
|
+
: `https://${trimmed}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
async function main() {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
slug: "demo",
|
|
13
|
-
contactName: "Jane Smith",
|
|
14
|
-
contactTitle: "Account Manager",
|
|
15
|
-
contactEmail: "jane@example.com",
|
|
16
|
-
supportEmail: "support@example.com",
|
|
17
|
-
websiteUrl: "https://example.com",
|
|
18
|
-
},
|
|
19
|
-
});
|
|
25
|
+
const organizationName = process.env.SHOWPANE_ORG_NAME?.trim();
|
|
26
|
+
if (!organizationName) {
|
|
27
|
+
console.log("Seed skipped: no organization details provided");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
20
30
|
|
|
21
|
-
const
|
|
31
|
+
const organizationSlug =
|
|
32
|
+
process.env.SHOWPANE_ORG_SLUG?.trim() || normalizeSlug(organizationName);
|
|
33
|
+
const contactName = process.env.SHOWPANE_CONTACT_NAME?.trim() || null;
|
|
34
|
+
const contactEmail = process.env.SHOWPANE_CONTACT_EMAIL?.trim() || null;
|
|
35
|
+
const websiteUrl = normalizeWebsiteUrl(process.env.SHOWPANE_WEBSITE_URL);
|
|
22
36
|
|
|
23
|
-
await prisma.
|
|
24
|
-
where: {
|
|
25
|
-
|
|
26
|
-
organizationId: org.id,
|
|
27
|
-
slug: "example",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
update: {
|
|
31
|
-
companyName: "Acme Health",
|
|
32
|
-
username: "example",
|
|
33
|
-
passwordHash,
|
|
34
|
-
lastUpdated: "2 April 2026",
|
|
35
|
-
},
|
|
37
|
+
const org = await prisma.organization.upsert({
|
|
38
|
+
where: { slug: organizationSlug },
|
|
39
|
+
update: {},
|
|
36
40
|
create: {
|
|
37
|
-
|
|
38
|
-
slug:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
name: organizationName,
|
|
42
|
+
slug: organizationSlug,
|
|
43
|
+
contactName,
|
|
44
|
+
contactTitle: null,
|
|
45
|
+
contactEmail,
|
|
46
|
+
supportEmail: contactEmail,
|
|
47
|
+
websiteUrl,
|
|
43
48
|
},
|
|
44
49
|
});
|
|
45
50
|
|
|
46
|
-
console.log(
|
|
51
|
+
console.log(`Seed complete: org '${org.slug}'`);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
main()
|
|
@@ -2,6 +2,8 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
ANALYTICS_METADATA_KEYS,
|
|
5
|
+
ORGANIZATION_NOT_READY_ERROR,
|
|
6
|
+
ORGANIZATION_REQUIRED_ERROR,
|
|
5
7
|
isPortalEventType,
|
|
6
8
|
toCloudPortalEventPayload,
|
|
7
9
|
} from "@/lib/portal-contracts";
|
|
@@ -29,4 +31,9 @@ describe("portal contracts", () => {
|
|
|
29
31
|
metadata: { durationSeconds: 12 },
|
|
30
32
|
});
|
|
31
33
|
});
|
|
34
|
+
|
|
35
|
+
it("exports the shared cloud auth error codes", () => {
|
|
36
|
+
expect(ORGANIZATION_REQUIRED_ERROR).toBe("organization_required");
|
|
37
|
+
expect(ORGANIZATION_NOT_READY_ERROR).toBe("organization_not_ready");
|
|
38
|
+
});
|
|
32
39
|
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Reference portal — the /portal
|
|
4
|
+
* Reference portal — the /portal-create skill reads this file as the quality
|
|
5
5
|
* and style guide when generating new portals. Keep it polished.
|
|
6
|
-
* Login: username "example", password "demo-only-password" (seeded by prisma/seed.ts)
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { type ReactNode } from "react";
|
|
@@ -5,14 +5,15 @@ import { PortalLogin } from "@/components/portal-login";
|
|
|
5
5
|
export default function ClientLogin() {
|
|
6
6
|
return (
|
|
7
7
|
<PortalLogin
|
|
8
|
-
companyName="
|
|
8
|
+
companyName="Your Portal"
|
|
9
9
|
companyLogo={
|
|
10
10
|
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-gray-900">
|
|
11
|
-
<span className="text-xs font-bold text-white">
|
|
11
|
+
<span className="text-xs font-bold text-white">P</span>
|
|
12
12
|
</div>
|
|
13
13
|
}
|
|
14
|
-
companyUrl="https://
|
|
15
|
-
supportEmail="support@
|
|
14
|
+
companyUrl="https://showpane.com"
|
|
15
|
+
supportEmail="support@showpane.com"
|
|
16
|
+
description="Private portal access. Sign in with the credentials you were sent."
|
|
16
17
|
/>
|
|
17
18
|
);
|
|
18
19
|
}
|
|
@@ -2,7 +2,13 @@ import { CopyButton } from "@/components/copy-button";
|
|
|
2
2
|
import { resolveDefaultOrganizationId } from "@/lib/client-portals";
|
|
3
3
|
import { prisma } from "@/lib/db";
|
|
4
4
|
import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ArrowUpRight,
|
|
7
|
+
BookOpen,
|
|
8
|
+
Command,
|
|
9
|
+
MessageSquareQuote,
|
|
10
|
+
Sparkles,
|
|
11
|
+
} from "lucide-react";
|
|
6
12
|
import Link from "next/link";
|
|
7
13
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
14
|
import os from "node:os";
|
|
@@ -60,7 +66,8 @@ export default async function Home() {
|
|
|
60
66
|
Your Showpane workspace is ready
|
|
61
67
|
</h1>
|
|
62
68
|
<p className="mt-4 max-w-2xl text-base leading-7 text-white/82 sm:text-lg">
|
|
63
|
-
Open a new terminal window,
|
|
69
|
+
Open a new terminal window, start Showpane with Claude, then run the
|
|
70
|
+
guided first-portal wizard.
|
|
64
71
|
</p>
|
|
65
72
|
</div>
|
|
66
73
|
</div>
|
|
@@ -72,7 +79,7 @@ export default async function Home() {
|
|
|
72
79
|
<div>
|
|
73
80
|
<div className="flex items-center gap-2 text-sm font-semibold text-white/80">
|
|
74
81
|
<Command className="h-4 w-4" />
|
|
75
|
-
Start with Claude
|
|
82
|
+
Step 1: Start with Claude
|
|
76
83
|
</div>
|
|
77
84
|
<p className="mt-3 text-sm leading-6 text-white/72">
|
|
78
85
|
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.
|
|
@@ -94,12 +101,41 @@ export default async function Home() {
|
|
|
94
101
|
</div>
|
|
95
102
|
</section>
|
|
96
103
|
|
|
104
|
+
<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">
|
|
105
|
+
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
|
106
|
+
<Sparkles className="h-4 w-4" />
|
|
107
|
+
Step 2: Run the first-portal wizard
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
111
|
+
If this is your first portal, use the guided path. It walks through
|
|
112
|
+
draft creation, preview, access setup, and the hosted publish handoff.
|
|
113
|
+
</p>
|
|
114
|
+
|
|
115
|
+
<div className="mt-5 flex items-start justify-between gap-4 rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
116
|
+
<div>
|
|
117
|
+
<p className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
|
118
|
+
Recommended
|
|
119
|
+
</p>
|
|
120
|
+
<p className="mt-3 font-mono text-sm leading-6 text-slate-700">
|
|
121
|
+
/portal-onboard
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
<CopyButton text="/portal-onboard" />
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
|
|
97
128
|
<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">
|
|
98
129
|
<div className="flex items-center gap-2 text-sm font-semibold text-slate-700">
|
|
99
130
|
<MessageSquareQuote className="h-4 w-4" />
|
|
100
|
-
|
|
131
|
+
Prefer the manual path?
|
|
101
132
|
</div>
|
|
102
133
|
|
|
134
|
+
<p className="mt-3 text-sm leading-6 text-slate-600">
|
|
135
|
+
Freeform prompts still work well once you know the shape you want.
|
|
136
|
+
Use them when you want the fast repeat-user path instead of the wizard.
|
|
137
|
+
</p>
|
|
138
|
+
|
|
103
139
|
<div className="mt-5 space-y-3">
|
|
104
140
|
{PROMPT_EXAMPLES.map((example, index) => (
|
|
105
141
|
<div
|
|
@@ -125,10 +161,11 @@ export default async function Home() {
|
|
|
125
161
|
<div className="max-w-xl">
|
|
126
162
|
<div className="flex items-center gap-2 text-sm font-semibold text-[#214668]">
|
|
127
163
|
<BookOpen className="h-4 w-4" />
|
|
128
|
-
Need
|
|
164
|
+
Need examples before you run the wizard?
|
|
129
165
|
</div>
|
|
130
166
|
<p className="mt-3 text-base leading-7 text-[#284f74]">
|
|
131
|
-
|
|
167
|
+
The first-portal guide now focuses on the recommended wizard path,
|
|
168
|
+
plus examples and manual prompting patterns.
|
|
132
169
|
</p>
|
|
133
170
|
</div>
|
|
134
171
|
<a
|
|
@@ -180,6 +180,7 @@ export function PortalShell({
|
|
|
180
180
|
const [copied, setCopied] = useState(false);
|
|
181
181
|
const [copyError, setCopyError] = useState(false);
|
|
182
182
|
const [visitorId] = useState(() => getOrCreateVisitorId());
|
|
183
|
+
const [showLocalBanner, setShowLocalBanner] = useState(false);
|
|
183
184
|
|
|
184
185
|
useEffect(() => {
|
|
185
186
|
const syncFromHash = () => setActiveTab(readHashTab(tabIds));
|
|
@@ -200,6 +201,12 @@ export function PortalShell({
|
|
|
200
201
|
return () => window.clearTimeout(timeout);
|
|
201
202
|
}, [copied, copyError]);
|
|
202
203
|
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (typeof window === "undefined") return;
|
|
206
|
+
const host = window.location.hostname;
|
|
207
|
+
setShowLocalBanner(host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0");
|
|
208
|
+
}, []);
|
|
209
|
+
|
|
203
210
|
useSectionTimeTracking(activeTab, resolvedEventsEndpoint, visitorId);
|
|
204
211
|
|
|
205
212
|
function switchTab(tab: string) {
|
|
@@ -232,6 +239,22 @@ export function PortalShell({
|
|
|
232
239
|
return (
|
|
233
240
|
<div className="flex min-h-screen flex-col bg-gray-50">
|
|
234
241
|
<div className="sticky top-0 z-30 border-b border-gray-200 bg-white/95 backdrop-blur">
|
|
242
|
+
{showLocalBanner && (
|
|
243
|
+
<div className="border-b border-amber-200 bg-amber-50">
|
|
244
|
+
<div className="mx-auto flex max-w-4xl items-center gap-2 px-4 py-2 text-[11px] font-medium text-amber-900 sm:px-6 sm:text-xs">
|
|
245
|
+
<span className="inline-flex shrink-0 whitespace-nowrap rounded-full bg-amber-200 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-amber-900 sm:text-[11px]">
|
|
246
|
+
Local preview
|
|
247
|
+
</span>
|
|
248
|
+
<span>
|
|
249
|
+
This portal is local only. Tell Claude{" "}
|
|
250
|
+
<code className="rounded bg-white px-1.5 py-0.5 font-mono text-[11px] text-amber-950 sm:text-xs">
|
|
251
|
+
/portal-deploy
|
|
252
|
+
</code>{" "}
|
|
253
|
+
to publish it to Showpane Cloud.
|
|
254
|
+
</span>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
235
258
|
<header className="border-b bg-white/90">
|
|
236
259
|
<div className="mx-auto flex max-w-4xl items-center justify-between px-4 py-3 sm:px-6">
|
|
237
260
|
<div className="flex items-center gap-3">
|
|
@@ -12,6 +12,39 @@ export const PORTAL_EVENT_TYPES = [
|
|
|
12
12
|
|
|
13
13
|
export type PortalEventType = (typeof PORTAL_EVENT_TYPES)[number];
|
|
14
14
|
export const ORGANIZATION_REQUIRED_ERROR = "organization_required" as const;
|
|
15
|
+
export const ORGANIZATION_NOT_READY_ERROR = "organization_not_ready" as const;
|
|
16
|
+
|
|
17
|
+
export const WORKSPACE_NEXT_ACTIONS = [
|
|
18
|
+
"open_checkout",
|
|
19
|
+
"wait_for_provisioning",
|
|
20
|
+
"open_settings",
|
|
21
|
+
"manage_billing",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export type WorkspaceNextAction = (typeof WORKSPACE_NEXT_ACTIONS)[number];
|
|
25
|
+
|
|
26
|
+
export const WORKSPACE_READINESS_REASONS = [
|
|
27
|
+
"billing_inactive",
|
|
28
|
+
"provisioning",
|
|
29
|
+
"provisioning_issue",
|
|
30
|
+
"workspace_incomplete",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
export type WorkspaceReadinessReason =
|
|
34
|
+
(typeof WORKSPACE_READINESS_REASONS)[number];
|
|
35
|
+
|
|
36
|
+
export interface OrganizationNotReadyPayload {
|
|
37
|
+
code: typeof ORGANIZATION_NOT_READY_ERROR;
|
|
38
|
+
error: string;
|
|
39
|
+
orgSlug: string;
|
|
40
|
+
provisioningStatus: string | null;
|
|
41
|
+
subscriptionStatus: string | null;
|
|
42
|
+
isActive: boolean;
|
|
43
|
+
checkoutUrl?: string;
|
|
44
|
+
settingsUrl: string;
|
|
45
|
+
nextAction: WorkspaceNextAction;
|
|
46
|
+
reason: WorkspaceReadinessReason;
|
|
47
|
+
}
|
|
15
48
|
|
|
16
49
|
export const ANALYTICS_METADATA_KEYS = {
|
|
17
50
|
durationSeconds: "durationSeconds",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.4.15
|
package/bundle/toolchain/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.6 (requires app >= 0.2.6)
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
const ORGANIZATION_REQUIRED_ERROR = "organization_required";
|
|
5
|
+
const ORGANIZATION_NOT_READY_ERROR = "organization_not_ready";
|
|
6
|
+
|
|
7
|
+
type OrganizationNotReadyPayload = {
|
|
8
|
+
code: typeof ORGANIZATION_NOT_READY_ERROR;
|
|
9
|
+
error: string;
|
|
10
|
+
orgSlug: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
nextAction: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
4
15
|
type ShowpaneConfig = {
|
|
5
16
|
accessToken?: string;
|
|
6
17
|
};
|
|
@@ -50,7 +61,29 @@ async function main() {
|
|
|
50
61
|
});
|
|
51
62
|
|
|
52
63
|
if (!res.ok) {
|
|
53
|
-
|
|
64
|
+
const rawBody = await res.text();
|
|
65
|
+
let body: { code?: string } | null = null;
|
|
66
|
+
if (rawBody) {
|
|
67
|
+
try {
|
|
68
|
+
body = JSON.parse(rawBody) as { code?: string };
|
|
69
|
+
} catch {
|
|
70
|
+
body = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (body?.code === ORGANIZATION_REQUIRED_ERROR) {
|
|
74
|
+
fail("Showpane Cloud workspace required. Finish checkout, then retry.");
|
|
75
|
+
}
|
|
76
|
+
if (body?.code === ORGANIZATION_NOT_READY_ERROR) {
|
|
77
|
+
const details = body as OrganizationNotReadyPayload;
|
|
78
|
+
fail(
|
|
79
|
+
`Workspace ${details.orgSlug} is not ready: ${details.error} (${details.reason}, next: ${details.nextAction}).`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
fail(
|
|
83
|
+
rawBody
|
|
84
|
+
? `Could not fetch cloud project link (${res.status}): ${rawBody}`
|
|
85
|
+
: `Could not fetch cloud project link (${res.status})`,
|
|
86
|
+
);
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
const projectLink = await res.json() as CloudProjectLink;
|
|
@@ -5,55 +5,134 @@ CONFIG_DIR="${HOME}/.showpane"
|
|
|
5
5
|
CONFIG_FILE="${CONFIG_DIR}/config.json"
|
|
6
6
|
|
|
7
7
|
usage() {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
cat <<'EOF'
|
|
9
|
+
Usage: showpane-config <get|set|unset|has|path> <key> [value]
|
|
10
|
+
|
|
11
|
+
Commands:
|
|
12
|
+
get <key> Read a value from config
|
|
13
|
+
set <key> <value> Write a scalar value to config
|
|
14
|
+
unset <key> Remove a key from config
|
|
15
|
+
has <key> Exit 0 if key exists, 1 otherwise
|
|
16
|
+
path Print the config file path
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
- Keys support dot notation, e.g. "cloud.portalUrl"
|
|
20
|
+
- Values "true", "false", "null", and numeric literals are parsed as JSON scalars
|
|
21
|
+
- Everything else is stored as a string
|
|
22
|
+
EOF
|
|
15
23
|
exit 1
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
ensure_config() {
|
|
19
|
-
|
|
20
|
-
mkdir -p "$CONFIG_DIR"
|
|
21
|
-
fi
|
|
27
|
+
mkdir -p "$CONFIG_DIR"
|
|
22
28
|
if [ ! -f "$CONFIG_FILE" ]; then
|
|
23
29
|
echo '{}' > "$CONFIG_FILE"
|
|
24
30
|
chmod 600 "$CONFIG_FILE"
|
|
25
31
|
fi
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
py_get='
|
|
35
|
+
import json, sys
|
|
36
|
+
path, key = sys.argv[1], sys.argv[2]
|
|
37
|
+
with open(path) as fh:
|
|
38
|
+
data = json.load(fh)
|
|
39
|
+
node = data
|
|
40
|
+
for part in key.split("."):
|
|
41
|
+
if not isinstance(node, dict) or part not in node:
|
|
42
|
+
raise SystemExit(1)
|
|
43
|
+
node = node[part]
|
|
44
|
+
if isinstance(node, bool):
|
|
45
|
+
print("true" if node else "false")
|
|
46
|
+
elif node is None:
|
|
47
|
+
print("null")
|
|
48
|
+
elif isinstance(node, (dict, list)):
|
|
49
|
+
print(json.dumps(node))
|
|
50
|
+
else:
|
|
51
|
+
print(node)
|
|
52
|
+
'
|
|
53
|
+
|
|
54
|
+
py_has='
|
|
55
|
+
import json, sys
|
|
56
|
+
path, key = sys.argv[1], sys.argv[2]
|
|
57
|
+
with open(path) as fh:
|
|
58
|
+
data = json.load(fh)
|
|
59
|
+
node = data
|
|
60
|
+
for part in key.split("."):
|
|
61
|
+
if not isinstance(node, dict) or part not in node:
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
node = node[part]
|
|
64
|
+
raise SystemExit(0)
|
|
65
|
+
'
|
|
66
|
+
|
|
67
|
+
py_set='
|
|
68
|
+
import json, sys
|
|
69
|
+
path, key, raw = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
70
|
+
with open(path) as fh:
|
|
71
|
+
data = json.load(fh)
|
|
72
|
+
try:
|
|
73
|
+
value = json.loads(raw)
|
|
74
|
+
except Exception:
|
|
75
|
+
value = raw
|
|
76
|
+
node = data
|
|
77
|
+
parts = key.split(".")
|
|
78
|
+
for part in parts[:-1]:
|
|
79
|
+
current = node.get(part)
|
|
80
|
+
if not isinstance(current, dict):
|
|
81
|
+
current = {}
|
|
82
|
+
node[part] = current
|
|
83
|
+
node = current
|
|
84
|
+
node[parts[-1]] = value
|
|
85
|
+
with open(path, "w") as fh:
|
|
86
|
+
json.dump(data, fh, indent=2)
|
|
87
|
+
fh.write("\n")
|
|
88
|
+
'
|
|
89
|
+
|
|
90
|
+
py_unset='
|
|
91
|
+
import json, sys
|
|
92
|
+
path, key = sys.argv[1], sys.argv[2]
|
|
93
|
+
with open(path) as fh:
|
|
94
|
+
data = json.load(fh)
|
|
95
|
+
node = data
|
|
96
|
+
parts = key.split(".")
|
|
97
|
+
for part in parts[:-1]:
|
|
98
|
+
current = node.get(part)
|
|
99
|
+
if not isinstance(current, dict):
|
|
100
|
+
raise SystemExit(1)
|
|
101
|
+
node = current
|
|
102
|
+
if parts[-1] not in node:
|
|
103
|
+
raise SystemExit(1)
|
|
104
|
+
del node[parts[-1]]
|
|
105
|
+
with open(path, "w") as fh:
|
|
106
|
+
json.dump(data, fh, indent=2)
|
|
107
|
+
fh.write("\n")
|
|
108
|
+
'
|
|
109
|
+
|
|
28
110
|
case "${1:-}" in
|
|
29
111
|
get)
|
|
30
112
|
[ -z "${2:-}" ] && usage
|
|
31
113
|
ensure_config
|
|
32
|
-
|
|
33
|
-
python3 -c "
|
|
34
|
-
import json, sys
|
|
35
|
-
with open('$CONFIG_FILE') as f:
|
|
36
|
-
data = json.load(f)
|
|
37
|
-
key = '$2'
|
|
38
|
-
if key in data:
|
|
39
|
-
print(data[key])
|
|
40
|
-
else:
|
|
41
|
-
sys.exit(1)
|
|
42
|
-
"
|
|
114
|
+
python3 -c "$py_get" "$CONFIG_FILE" "$2"
|
|
43
115
|
;;
|
|
44
116
|
set)
|
|
45
117
|
[ -z "${2:-}" ] || [ -z "${3:-}" ] && usage
|
|
46
118
|
ensure_config
|
|
47
|
-
python3 -c "
|
|
48
|
-
import json
|
|
49
|
-
with open('$CONFIG_FILE') as f:
|
|
50
|
-
data = json.load(f)
|
|
51
|
-
data['$2'] = '$3'
|
|
52
|
-
with open('$CONFIG_FILE', 'w') as f:
|
|
53
|
-
json.dump(data, f, indent=2)
|
|
54
|
-
"
|
|
119
|
+
python3 -c "$py_set" "$CONFIG_FILE" "$2" "$3"
|
|
55
120
|
chmod 600 "$CONFIG_FILE"
|
|
56
121
|
;;
|
|
122
|
+
unset)
|
|
123
|
+
[ -z "${2:-}" ] && usage
|
|
124
|
+
ensure_config
|
|
125
|
+
python3 -c "$py_unset" "$CONFIG_FILE" "$2"
|
|
126
|
+
chmod 600 "$CONFIG_FILE"
|
|
127
|
+
;;
|
|
128
|
+
has)
|
|
129
|
+
[ -z "${2:-}" ] && usage
|
|
130
|
+
ensure_config
|
|
131
|
+
python3 -c "$py_has" "$CONFIG_FILE" "$2"
|
|
132
|
+
;;
|
|
133
|
+
path)
|
|
134
|
+
printf '%s\n' "$CONFIG_FILE"
|
|
135
|
+
;;
|
|
57
136
|
--help|-h)
|
|
58
137
|
usage
|
|
59
138
|
;;
|