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.
Files changed (55) hide show
  1. package/README.md +2 -1
  2. package/bundle/meta/scaffold-manifest.json +10 -10
  3. package/bundle/scaffold/VERSION +1 -1
  4. package/bundle/scaffold/prisma/seed.ts +40 -35
  5. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +7 -0
  6. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +1 -2
  7. package/bundle/scaffold/src/app/(portal)/client/page.tsx +5 -4
  8. package/bundle/scaffold/src/app/page.tsx +43 -6
  9. package/bundle/scaffold/src/components/portal-shell.tsx +23 -0
  10. package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
  11. package/bundle/toolchain/CLI_VERSION +1 -0
  12. package/bundle/toolchain/TELEMETRY_CONFIG.json +4 -0
  13. package/bundle/toolchain/VERSION +1 -1
  14. package/bundle/toolchain/bin/ensure-cloud-project-link.ts +34 -1
  15. package/bundle/toolchain/bin/showpane-config +108 -29
  16. package/bundle/toolchain/bin/showpane-telemetry-log +84 -0
  17. package/bundle/toolchain/bin/showpane-telemetry-sync +212 -0
  18. package/bundle/toolchain/bin/showpane-update-check +130 -0
  19. package/bundle/toolchain/skills/SKILL.md.tmpl +13 -0
  20. package/bundle/toolchain/skills/VERSION +1 -1
  21. package/bundle/toolchain/skills/portal-analytics/SKILL.md +60 -38
  22. package/bundle/toolchain/skills/portal-analytics/SKILL.md.tmpl +192 -0
  23. package/bundle/toolchain/skills/portal-create/SKILL.md +65 -67
  24. package/bundle/toolchain/skills/portal-create/SKILL.md.tmpl +264 -0
  25. package/bundle/toolchain/skills/portal-credentials/SKILL.md +66 -49
  26. package/bundle/toolchain/skills/portal-credentials/SKILL.md.tmpl +198 -0
  27. package/bundle/toolchain/skills/portal-delete/SKILL.md +63 -41
  28. package/bundle/toolchain/skills/portal-delete/SKILL.md.tmpl +194 -0
  29. package/bundle/toolchain/skills/portal-deploy/SKILL.md +57 -47
  30. package/bundle/toolchain/skills/portal-deploy/SKILL.md.tmpl +452 -0
  31. package/bundle/toolchain/skills/portal-dev/SKILL.md +65 -47
  32. package/bundle/toolchain/skills/portal-dev/SKILL.md.tmpl +228 -0
  33. package/bundle/toolchain/skills/portal-list/SKILL.md +64 -43
  34. package/bundle/toolchain/skills/portal-list/SKILL.md.tmpl +181 -0
  35. package/bundle/toolchain/skills/portal-onboard/SKILL.md +331 -162
  36. package/bundle/toolchain/skills/portal-onboard/SKILL.md.tmpl +340 -0
  37. package/bundle/toolchain/skills/portal-preview/SKILL.md +65 -44
  38. package/bundle/toolchain/skills/portal-preview/SKILL.md.tmpl +171 -0
  39. package/bundle/toolchain/skills/portal-setup/SKILL.md +79 -60
  40. package/bundle/toolchain/skills/portal-setup/SKILL.md.tmpl +227 -0
  41. package/bundle/toolchain/skills/portal-share/SKILL.md +69 -47
  42. package/bundle/toolchain/skills/portal-share/SKILL.md.tmpl +162 -0
  43. package/bundle/toolchain/skills/portal-status/SKILL.md +58 -37
  44. package/bundle/toolchain/skills/portal-status/SKILL.md.tmpl +196 -0
  45. package/bundle/toolchain/skills/portal-update/SKILL.md +60 -46
  46. package/bundle/toolchain/skills/portal-update/SKILL.md.tmpl +269 -0
  47. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +55 -33
  48. package/bundle/toolchain/skills/portal-upgrade/SKILL.md.tmpl +164 -0
  49. package/bundle/toolchain/skills/portal-verify/SKILL.md +69 -14
  50. package/bundle/toolchain/skills/portal-verify/SKILL.md.tmpl +224 -0
  51. package/bundle/toolchain/skills/shared/preamble.md +30 -126
  52. package/bundle/toolchain/skills/shared/runtime-principles.md +25 -0
  53. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +1 -1
  54. package/dist/index.js +79 -14
  55. 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 org creation and billing happen in the Showpane Cloud web app.
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-09T23:30:19.010Z",
4
- "scaffoldVersion": "0.2.4",
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": "1725bfa6524c5265e7c171cf06568417d39b947fff49c242f03859479c82334b",
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": "398b645c31ea0d5b0291f27c32aded22bdb64021e581a547e85b3cccca65c551",
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": "80066377d3281786c2bb9ecc857514124e094a2e66dca2fb08ded994c25fa2bc",
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": "b9b744f0358b7773f319757157598fc69059eb38e0eaa1c09cb5fc10cfc3894b",
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": "af36f1a6f359d6a7bd4a6ac550058c9d9c107e9885bb238b4c06ec26700e23e3",
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": "732ea54f313386b65bce1170785379b27bb26b5da26b833b1e50c3713b87be1a",
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": "a4e16e118ef93f79e71fb69e80f1fac6e6fff90f0fbdacdf8deb821a57656877",
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": "519a97afe3ae618077c95aba5a9764383e7edf6c9effd62a5638a50a0b2a676a",
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",
@@ -1 +1 @@
1
- 0.2.4
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 org = await prisma.organization.upsert({
8
- where: { slug: "demo" },
9
- update: {},
10
- create: {
11
- name: "Demo Company",
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 passwordHash = await bcrypt.hash("demo-only-password", 10);
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.clientPortal.upsert({
24
- where: {
25
- organizationId_slug: {
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
- organizationId: org.id,
38
- slug: "example",
39
- companyName: "Acme Health",
40
- username: "example",
41
- passwordHash,
42
- lastUpdated: "2 April 2026",
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("Seed complete: org 'demo', portal 'example' (username: example, password: demo-only-password)");
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 create skill reads this file as the quality
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="Demo Company"
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">D</span>
11
+ <span className="text-xs font-bold text-white">P</span>
12
12
  </div>
13
13
  }
14
- companyUrl="https://example.com"
15
- supportEmail="support@example.com"
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 { ArrowUpRight, BookOpen, Command, MessageSquareQuote } from "lucide-react";
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, run Showpane with Claude, and create your first client portal.
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
- What to say to Claude
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 help creating your first portal?
164
+ Need examples before you run the wizard?
129
165
  </div>
130
166
  <p className="mt-3 text-base leading-7 text-[#284f74]">
131
- Follow the step-by-step guide with examples, best practices, and a walkthrough video.
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
@@ -0,0 +1,4 @@
1
+ {
2
+ "baseUrl": "https://app.showpane.com",
3
+ "ingestPath": "/api/telemetry/ingest"
4
+ }
@@ -1 +1 @@
1
- 1.1.4 (requires app >= 0.2.4)
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
- fail(`Could not fetch cloud project link (${res.status})`);
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
- echo "Usage: showpane-config <get|set> <key> [value]"
9
- echo ""
10
- echo "Commands:"
11
- echo " get <key> Read a value from config"
12
- echo " set <key> <value> Write a value to config"
13
- echo ""
14
- echo "Config file: ${CONFIG_FILE}"
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
- if [ ! -d "$CONFIG_DIR" ]; then
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
- # Use python3 for portable JSON parsing (available on macOS + Linux)
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
  ;;