showpane 0.4.16 → 0.4.18

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 (28) hide show
  1. package/bundle/meta/scaffold-manifest.json +6 -6
  2. package/bundle/scaffold/VERSION +1 -1
  3. package/bundle/scaffold/prisma/seed.ts +2 -1
  4. package/bundle/scaffold/src/app/(portal)/client/page.tsx +81 -8
  5. package/bundle/scaffold/src/middleware.ts +13 -3
  6. package/bundle/toolchain/CLI_VERSION +1 -1
  7. package/bundle/toolchain/VERSION +1 -1
  8. package/bundle/toolchain/bin/get-org.ts +46 -0
  9. package/bundle/toolchain/skills/VERSION +1 -1
  10. package/bundle/toolchain/skills/portal-analytics/SKILL.md +11 -0
  11. package/bundle/toolchain/skills/portal-create/SKILL.md +63 -16
  12. package/bundle/toolchain/skills/portal-create/SKILL.md.tmpl +52 -16
  13. package/bundle/toolchain/skills/portal-credentials/SKILL.md +11 -0
  14. package/bundle/toolchain/skills/portal-delete/SKILL.md +11 -0
  15. package/bundle/toolchain/skills/portal-deploy/SKILL.md +27 -2
  16. package/bundle/toolchain/skills/portal-deploy/SKILL.md.tmpl +16 -2
  17. package/bundle/toolchain/skills/portal-dev/SKILL.md +11 -0
  18. package/bundle/toolchain/skills/portal-list/SKILL.md +11 -0
  19. package/bundle/toolchain/skills/portal-onboard/SKILL.md +41 -10
  20. package/bundle/toolchain/skills/portal-onboard/SKILL.md.tmpl +37 -2
  21. package/bundle/toolchain/skills/portal-preview/SKILL.md +11 -0
  22. package/bundle/toolchain/skills/portal-share/SKILL.md +11 -0
  23. package/bundle/toolchain/skills/portal-status/SKILL.md +11 -0
  24. package/bundle/toolchain/skills/portal-update/SKILL.md +11 -0
  25. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +11 -0
  26. package/bundle/toolchain/skills/portal-verify/SKILL.md +11 -0
  27. package/dist/index.js +54 -10
  28. package/package.json +1 -1
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-04-11T21:51:10.517Z",
4
- "scaffoldVersion": "0.2.6",
3
+ "generatedAt": "2026-04-11T23:07:20.944Z",
4
+ "scaffoldVersion": "0.2.7",
5
5
  "files": {
6
6
  ".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
7
7
  ".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
8
- "VERSION": "be3c6d2c6c406a64d44f0b6464a887e290416dd90c524094485b1be00936d6d7",
8
+ "VERSION": "5b57e3b8c153d1d33b0c0f1ee29d3de1bf93232ed95aa78b80fe885e99faa915",
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": "bb8eb3bc189fabbab3be83d3909afee583a5b724d258139feda22af504bed134",
15
+ "prisma/seed.ts": "98f7d5fd97ce2ff66bdf060ccb6695416d18ac983672b256d5d95d315ccee6e5",
16
16
  "public/example-avatar.svg": "0edeb0d3fbefa89cc27ffe6564d20e3ee0fd073cb6d9f2a025248ef3b3f277fd",
17
17
  "public/example-logo.svg": "bc5cd933aff2a17698dee66a7b4ea940ad12238e9d813474d643b459b1e8d6da",
18
18
  "public/robots.txt": "331ea9090db0c9f6f597bd9840fd5b171830f6e0b3ba1cb24dfa91f0c95aedc1",
@@ -27,7 +27,7 @@
27
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": "7b4e3e5b286672f83581e24349e98ac930d8bbf5dfed02793754be2ddadf430f",
30
+ "src/app/(portal)/client/page.tsx": "7efe97c047e9ad8f5d9034fe616c0c915277a9cb92fa4ce57fe35af672c80320",
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",
@@ -57,7 +57,7 @@
57
57
  "src/lib/storage.ts": "ae3b85fc6cccd39d4174a391dcbe6e91fb9460eb407ec9dbfedd63594a441d08",
58
58
  "src/lib/token.ts": "518898ca3cbba069f507f736ed80f5afa0c5af07b0b02fdd2d682e466598c803",
59
59
  "src/lib/utils.ts": "d1f1e0d62cb8d8d1e04c26e14de842d8a151f75812d81b046c65b5d1fe8e4b27",
60
- "src/middleware.ts": "5623ef170d9327e47373943eae33a8b07a6dda970525d0833e0865e4429df094",
60
+ "src/middleware.ts": "5925c0b1ad801a201e82c26ccd6c0642717670446d1503e9ed82dcf3ea03c825",
61
61
  "src/types/adm-zip.d.ts": "a9a32ea84d6d6cd89626ba5cd6f5519158a652362abbe5647474114c30ecc3c4",
62
62
  "tailwind.config.ts": "dc0dec22249b290b857190495ce7140327a6f4d94fedd5dffcdb298fa3928071",
63
63
  "tests/.gitkeep": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
@@ -1 +1 @@
1
- 0.2.6
1
+ 0.2.7
@@ -33,6 +33,7 @@ async function main() {
33
33
  const contactName = process.env.SHOWPANE_CONTACT_NAME?.trim() || null;
34
34
  const contactEmail = process.env.SHOWPANE_CONTACT_EMAIL?.trim() || null;
35
35
  const websiteUrl = normalizeWebsiteUrl(process.env.SHOWPANE_WEBSITE_URL);
36
+ const contactTitle = "Point of contact";
36
37
 
37
38
  const org = await prisma.organization.upsert({
38
39
  where: { slug: organizationSlug },
@@ -41,7 +42,7 @@ async function main() {
41
42
  name: organizationName,
42
43
  slug: organizationSlug,
43
44
  contactName,
44
- contactTitle: null,
45
+ contactTitle,
45
46
  contactEmail,
46
47
  supportEmail: contactEmail,
47
48
  websiteUrl,
@@ -1,19 +1,92 @@
1
- "use client";
2
-
3
1
  import { PortalLogin } from "@/components/portal-login";
2
+ import { getInitialLogo } from "@/lib/branding";
3
+ import { prisma } from "@/lib/db";
4
+ import { resolveDefaultOrganizationId } from "@/lib/client-portals";
5
+ import { getRuntimePortalBySlug, getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
6
+
7
+ export default async function ClientLogin({
8
+ searchParams,
9
+ }: {
10
+ searchParams?: Promise<{ portal?: string }>;
11
+ }) {
12
+ const params = (await searchParams) ?? {};
13
+ const portalSlug = params.portal?.trim() || null;
14
+
15
+ let companyName = "Your Portal";
16
+ let companyUrl = "https://showpane.com";
17
+ let supportEmail = "support@showpane.com";
18
+ let portalLabel = "Client Portal";
19
+ let description = "Private portal access. Sign in with the credentials you were sent.";
20
+ let companyInitial = "P";
21
+
22
+ if (portalSlug) {
23
+ try {
24
+ if (isRuntimeSnapshotMode()) {
25
+ const state = await getRuntimeState();
26
+ const portal = await getRuntimePortalBySlug(portalSlug);
27
+ if (portal) {
28
+ companyName = portal.companyName;
29
+ portalLabel = `${portal.companyName} Portal`;
30
+ description = `Private portal for ${portal.companyName}. Sign in with the credentials you were sent.`;
31
+ companyInitial = portal.companyName[0]?.toUpperCase() || "P";
32
+ if (state?.organization?.websiteUrl) {
33
+ companyUrl = state.organization.websiteUrl;
34
+ }
35
+ if (state?.organization?.supportEmail) {
36
+ supportEmail = state.organization.supportEmail;
37
+ }
38
+ }
39
+ } else {
40
+ const organizationId = await resolveDefaultOrganizationId();
41
+ if (organizationId) {
42
+ const portal = await prisma.clientPortal.findFirst({
43
+ where: {
44
+ organizationId,
45
+ slug: portalSlug,
46
+ isActive: true,
47
+ },
48
+ select: { companyName: true },
49
+ });
50
+ const organization = await prisma.organization.findUnique({
51
+ where: { id: organizationId },
52
+ select: {
53
+ websiteUrl: true,
54
+ supportEmail: true,
55
+ portalLabel: true,
56
+ },
57
+ });
58
+
59
+ if (portal) {
60
+ companyName = portal.companyName;
61
+ portalLabel = organization?.portalLabel || `${portal.companyName} Portal`;
62
+ description = `Private portal for ${portal.companyName}. Sign in with the credentials you were sent.`;
63
+ companyInitial = portal.companyName[0]?.toUpperCase() || "P";
64
+ }
65
+ if (organization?.websiteUrl) {
66
+ companyUrl = organization.websiteUrl;
67
+ }
68
+ if (organization?.supportEmail) {
69
+ supportEmail = organization.supportEmail;
70
+ }
71
+ }
72
+ }
73
+ } catch {
74
+ // Fall back to the generic login shell if portal-aware context cannot be resolved.
75
+ }
76
+ }
4
77
 
5
- export default function ClientLogin() {
6
78
  return (
7
79
  <PortalLogin
8
- companyName="Your Portal"
80
+ companyName={companyName}
9
81
  companyLogo={
10
82
  <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">P</span>
83
+ <span className="text-xs font-bold text-white">{companyInitial}</span>
12
84
  </div>
13
85
  }
14
- companyUrl="https://showpane.com"
15
- supportEmail="support@showpane.com"
16
- description="Private portal access. Sign in with the credentials you were sent."
86
+ companyUrl={companyUrl}
87
+ supportEmail={supportEmail}
88
+ portalLabel={portalLabel}
89
+ description={description}
17
90
  />
18
91
  );
19
92
  }
@@ -35,18 +35,28 @@ export async function middleware(req: NextRequest) {
35
35
  try {
36
36
  const portal = await getAuthenticatedPortal(req);
37
37
  if (!portal) {
38
- return NextResponse.redirect(new URL("/client", req.url));
38
+ const loginUrl = new URL("/client", req.url);
39
+ const requestedSlug = pathname.split("/")[2];
40
+ if (requestedSlug) {
41
+ loginUrl.searchParams.set("portal", requestedSlug);
42
+ }
43
+ return NextResponse.redirect(loginUrl);
39
44
  }
40
45
  // Ensure the URL slug matches the authenticated slug
41
46
  const urlSlug = pathname.split("/")[2];
42
47
  if (urlSlug !== portal.slug) {
43
- return NextResponse.redirect(new URL("/client", req.url));
48
+ return NextResponse.redirect(new URL(`/client/${portal.slug}`, req.url));
44
49
  }
45
50
  return NextResponse.next();
46
51
  } catch (e) {
47
52
  console.error("Middleware: DB error checking auth on portal page", e);
48
53
  // Fail closed — redirect to login
49
- return NextResponse.redirect(new URL("/client", req.url));
54
+ const loginUrl = new URL("/client", req.url);
55
+ const requestedSlug = pathname.split("/")[2];
56
+ if (requestedSlug) {
57
+ loginUrl.searchParams.set("portal", requestedSlug);
58
+ }
59
+ return NextResponse.redirect(loginUrl);
50
60
  }
51
61
  }
52
62
 
@@ -1 +1 @@
1
- 0.4.16
1
+ 0.4.18
@@ -1 +1 @@
1
- 1.1.6 (requires app >= 0.2.6)
1
+ 1.1.7 (requires app >= 0.2.7)
@@ -0,0 +1,46 @@
1
+ import { PrismaClient } from "@/lib/prisma-client";
2
+
3
+ function fail(message: string): never {
4
+ console.error(JSON.stringify({ ok: false, error: message }));
5
+ process.exit(1);
6
+ }
7
+
8
+ async function main() {
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args.includes("--help")) {
12
+ console.log("Usage: get-org --slug <orgSlug>");
13
+ console.log("Looks up a local Showpane organization by slug.");
14
+ process.exit(0);
15
+ }
16
+
17
+ const getArg = (flag: string) => {
18
+ const index = args.indexOf(flag);
19
+ return index !== -1 ? args[index + 1] : undefined;
20
+ };
21
+
22
+ const slug = getArg("--slug");
23
+ if (!slug) {
24
+ fail("Missing --slug");
25
+ }
26
+
27
+ const prisma = new PrismaClient();
28
+ try {
29
+ const org = await prisma.organization.findUnique({
30
+ where: { slug },
31
+ select: { id: true, slug: true, name: true },
32
+ });
33
+
34
+ if (!org) {
35
+ fail(`Organization "${slug}" not found`);
36
+ }
37
+
38
+ console.log(JSON.stringify({ ok: true, org }));
39
+ } finally {
40
+ await prisma.$disconnect();
41
+ }
42
+ }
43
+
44
+ main().catch((error) => {
45
+ fail(String(error));
46
+ });
@@ -1 +1 @@
1
- 1.1.6 (requires app >= 0.2.6)
1
+ 1.1.7 (requires app >= 0.2.7)
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-analytics","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-analytics" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -27,6 +27,10 @@ fi
27
27
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
28
28
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
29
29
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
30
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
31
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
32
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
33
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
30
34
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
31
35
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
32
36
  DATABASE_URL="${DATABASE_URL:-}"
@@ -67,6 +71,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
67
71
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
68
72
  echo '{"skill":"portal-create","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
69
73
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
74
+ if [ "portal-create" = "portal-deploy" ]; then
75
+ echo "ORG_SLUG: $ORG_SLUG"
76
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
77
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
78
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
79
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
80
+ fi
70
81
  echo "TELEMETRY: $TEL"
71
82
  echo "TEL_PROMPTED: $TEL_PROMPTED"
72
83
  ```
@@ -99,14 +110,45 @@ If `skills/shared/platform-constraints.md` exists, read it once near the start o
99
110
 
100
111
  ## Steps
101
112
 
102
- ### Step 1: Determine the portal slug
113
+ ### Step 1: Resolve the local organization
114
+
115
+ Resolve the organization from the configured `ORG_SLUG` before doing anything else:
116
+
117
+ ```bash
118
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/get-org.ts" --slug "$ORG_SLUG"
119
+ ```
120
+
121
+ The helper returns:
122
+
123
+ ```json
124
+ {"ok":true,"org":{"id":"...","slug":"...","name":"..."}}
125
+ ```
126
+
127
+ Store:
128
+
129
+ - `ORG_ID`
130
+ - `ORG_NAME`
131
+
132
+ If the helper fails, stop and tell the user to run `/portal-setup` again instead
133
+ of guessing with ad-hoc SQL.
134
+
135
+ Do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or template
136
+ directories just to understand the project. The canonical references for this skill are:
137
+
138
+ - the configured `APP_PATH`
139
+ - the configured `ORG_SLUG`
140
+ - this skill file
141
+ - `$SKILL_DIR/templates/<chosen-template>/...`
142
+ - `$APP_PATH/src/app/(portal)/client/example/example-client.tsx`
143
+
144
+ ### Step 2: Determine the portal slug
103
145
 
104
146
  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.
105
147
 
106
148
  Validate the slug by running:
107
149
 
108
150
  ```bash
109
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/check-slug.ts" --slug <slug> --org-id <org_id>
151
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/check-slug.ts" --slug <slug> --org-id "$ORG_ID"
110
152
  ```
111
153
 
112
154
  The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":"..."}`. If invalid:
@@ -120,7 +162,7 @@ Also ask for the client's website domain (e.g., "acme-health.com"). This is opti
120
162
  - If provided, the client logo will be fetched via `getLogoUrl(domain)` and stored in `ClientPortal.logoUrl`
121
163
  - If not provided, an initial-based logo is generated via `getInitialLogo(companyName)` and stored as a data URI
122
164
 
123
- ### Step 2: Granola MCP integration (optional)
165
+ ### Step 3: Granola MCP integration (optional)
124
166
 
125
167
  Try to use the Granola MCP `list_meetings` tool to fetch recent meetings. This is a convenience, not a requirement.
126
168
 
@@ -139,7 +181,7 @@ Try to use the Granola MCP `list_meetings` tool to fetch recent meetings. This i
139
181
 
140
182
  Never fail or block because Granola is unavailable. It is purely additive.
141
183
 
142
- ### Step 3: Template selection
184
+ ### Step 4: Template selection
143
185
 
144
186
  Ask which template to use as a starting point. Keep this brief and practical —
145
187
  the user chose `/portal-create` because they want the fast path, not a wizard.
@@ -163,7 +205,7 @@ cat "$APP_PATH/src/app/(portal)/client/example/example-client.tsx"
163
205
 
164
206
  The template provides content structure. The example provides quality and styling. Match the example's patterns: card styles, typography, spacing, responsive breakpoints. Templates are inspiration, not rigid scaffolds. Adapt the structure to fit the actual content.
165
207
 
166
- ### Step 4: Analyze transcript (if available)
208
+ ### Step 5: Analyze transcript (if available)
167
209
 
168
210
  If a transcript was provided (from Granola or pasted), analyze it to extract:
169
211
 
@@ -189,7 +231,7 @@ Extract from the transcript:
189
231
  - **Mentioned documents** (for documents tab)
190
232
  - **Services or products discussed** (for services/overview content)
191
233
 
192
- ### Step 5: Generate the portal files
234
+ ### Step 6: Generate the portal files
193
235
 
194
236
  Create two files in `$APP_PATH/src/app/(portal)/client/<slug>/`:
195
237
 
@@ -275,17 +317,22 @@ Import only the icons you need from `lucide-react`. Common choices:
275
317
  </details>
276
318
  ```
277
319
 
278
- ### Step 6: Create database record
320
+ ### Step 7: Create database record
279
321
 
280
322
  Run the create-portal script to register the portal in the database:
281
323
 
282
324
  ```bash
283
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id <org_id>
325
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID"
284
326
  ```
285
327
 
286
- This creates the `ClientPortal` record with the slug, company name, and links it to the Organization. It does NOT create credentials — that is a separate step via `/portal-credentials`.
328
+ This creates the `ClientPortal` record with the slug and company name, links it to
329
+ the Organization, and currently auto-generates initial credentials. The script
330
+ returns `username` and `password`.
331
+
332
+ Do not dump those credentials into the middle of the flow unless the user asked.
333
+ For onboarding, carry them forward quietly and show them at the access phase.
287
334
 
288
- ### Step 7: Self-review
335
+ ### Step 8: Self-review
289
336
 
290
337
  After generating the files, read them back and verify:
291
338
 
@@ -300,7 +347,7 @@ After generating the files, read them back and verify:
300
347
 
301
348
  If any check fails, fix the issue before proceeding.
302
349
 
303
- ### Step 8: Open preview
350
+ ### Step 9: Open preview
304
351
 
305
352
  Check if the dev server is running:
306
353
 
@@ -318,7 +365,7 @@ If not running, suggest:
318
365
 
319
366
  > "Start the dev server with `/portal-dev` to preview your portal at http://localhost:3000/client/<slug>"
320
367
 
321
- ### Step 9: Summary and next steps
368
+ ### Step 10: Summary and next steps
322
369
 
323
370
  Print a summary:
324
371
 
@@ -331,13 +378,13 @@ Portal created: <slug>
331
378
  src/app/(portal)/client/<slug>/<slug>-client.tsx
332
379
 
333
380
  Next steps:
334
- 1. Create login credentials: /portal-credentials <slug>
335
- 2. Preview the portal: /portal-preview <slug>
336
- 3. Edit content: /portal-update <slug>
381
+ 1. Preview the portal: /portal-preview <slug>
382
+ 2. Edit content: /portal-update <slug>
383
+ 3. Rotate credentials: /portal-credentials <slug>
337
384
  4. Deploy: /portal-deploy
338
385
  ```
339
386
 
340
- ### Step 10: Record learning
387
+ ### Step 11: Record learning
341
388
 
342
389
  Append a learning about the portal creation for future reference:
343
390
 
@@ -15,14 +15,45 @@ capabilities, read `skills/shared/platform-constraints.md` and apply the relevan
15
15
 
16
16
  ## Steps
17
17
 
18
- ### Step 1: Determine the portal slug
18
+ ### Step 1: Resolve the local organization
19
+
20
+ Resolve the organization from the configured `ORG_SLUG` before doing anything else:
21
+
22
+ ```bash
23
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/get-org.ts" --slug "$ORG_SLUG"
24
+ ```
25
+
26
+ The helper returns:
27
+
28
+ ```json
29
+ {"ok":true,"org":{"id":"...","slug":"...","name":"..."}}
30
+ ```
31
+
32
+ Store:
33
+
34
+ - `ORG_ID`
35
+ - `ORG_NAME`
36
+
37
+ If the helper fails, stop and tell the user to run `/portal-setup` again instead
38
+ of guessing with ad-hoc SQL.
39
+
40
+ Do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or template
41
+ directories just to understand the project. The canonical references for this skill are:
42
+
43
+ - the configured `APP_PATH`
44
+ - the configured `ORG_SLUG`
45
+ - this skill file
46
+ - `$SKILL_DIR/templates/<chosen-template>/...`
47
+ - `$APP_PATH/src/app/(portal)/client/example/example-client.tsx`
48
+
49
+ ### Step 2: Determine the portal slug
19
50
 
20
51
  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.
21
52
 
22
53
  Validate the slug by running:
23
54
 
24
55
  ```bash
25
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/check-slug.ts" --slug <slug> --org-id <org_id>
56
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/check-slug.ts" --slug <slug> --org-id "$ORG_ID"
26
57
  ```
27
58
 
28
59
  The script returns `{"valid":true}` or `{"valid":false,"reason":"...","message":"..."}`. If invalid:
@@ -36,7 +67,7 @@ Also ask for the client's website domain (e.g., "acme-health.com"). This is opti
36
67
  - If provided, the client logo will be fetched via `getLogoUrl(domain)` and stored in `ClientPortal.logoUrl`
37
68
  - If not provided, an initial-based logo is generated via `getInitialLogo(companyName)` and stored as a data URI
38
69
 
39
- ### Step 2: Granola MCP integration (optional)
70
+ ### Step 3: Granola MCP integration (optional)
40
71
 
41
72
  Try to use the Granola MCP `list_meetings` tool to fetch recent meetings. This is a convenience, not a requirement.
42
73
 
@@ -55,7 +86,7 @@ Try to use the Granola MCP `list_meetings` tool to fetch recent meetings. This i
55
86
 
56
87
  Never fail or block because Granola is unavailable. It is purely additive.
57
88
 
58
- ### Step 3: Template selection
89
+ ### Step 4: Template selection
59
90
 
60
91
  Ask which template to use as a starting point. Keep this brief and practical —
61
92
  the user chose `/portal-create` because they want the fast path, not a wizard.
@@ -79,7 +110,7 @@ cat "$APP_PATH/src/app/(portal)/client/example/example-client.tsx"
79
110
 
80
111
  The template provides content structure. The example provides quality and styling. Match the example's patterns: card styles, typography, spacing, responsive breakpoints. Templates are inspiration, not rigid scaffolds. Adapt the structure to fit the actual content.
81
112
 
82
- ### Step 4: Analyze transcript (if available)
113
+ ### Step 5: Analyze transcript (if available)
83
114
 
84
115
  If a transcript was provided (from Granola or pasted), analyze it to extract:
85
116
 
@@ -105,7 +136,7 @@ Extract from the transcript:
105
136
  - **Mentioned documents** (for documents tab)
106
137
  - **Services or products discussed** (for services/overview content)
107
138
 
108
- ### Step 5: Generate the portal files
139
+ ### Step 6: Generate the portal files
109
140
 
110
141
  Create two files in `$APP_PATH/src/app/(portal)/client/<slug>/`:
111
142
 
@@ -191,17 +222,22 @@ Import only the icons you need from `lucide-react`. Common choices:
191
222
  </details>
192
223
  ```
193
224
 
194
- ### Step 6: Create database record
225
+ ### Step 7: Create database record
195
226
 
196
227
  Run the create-portal script to register the portal in the database:
197
228
 
198
229
  ```bash
199
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id <org_id>
230
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-portal.ts" --slug <slug> --company "<client_company_name>" --org-id "$ORG_ID"
200
231
  ```
201
232
 
202
- This creates the `ClientPortal` record with the slug, company name, and links it to the Organization. It does NOT create credentials — that is a separate step via `/portal-credentials`.
233
+ This creates the `ClientPortal` record with the slug and company name, links it to
234
+ the Organization, and currently auto-generates initial credentials. The script
235
+ returns `username` and `password`.
236
+
237
+ Do not dump those credentials into the middle of the flow unless the user asked.
238
+ For onboarding, carry them forward quietly and show them at the access phase.
203
239
 
204
- ### Step 7: Self-review
240
+ ### Step 8: Self-review
205
241
 
206
242
  After generating the files, read them back and verify:
207
243
 
@@ -216,7 +252,7 @@ After generating the files, read them back and verify:
216
252
 
217
253
  If any check fails, fix the issue before proceeding.
218
254
 
219
- ### Step 8: Open preview
255
+ ### Step 9: Open preview
220
256
 
221
257
  Check if the dev server is running:
222
258
 
@@ -234,7 +270,7 @@ If not running, suggest:
234
270
 
235
271
  > "Start the dev server with `/portal-dev` to preview your portal at http://localhost:3000/client/<slug>"
236
272
 
237
- ### Step 9: Summary and next steps
273
+ ### Step 10: Summary and next steps
238
274
 
239
275
  Print a summary:
240
276
 
@@ -247,13 +283,13 @@ Portal created: <slug>
247
283
  src/app/(portal)/client/<slug>/<slug>-client.tsx
248
284
 
249
285
  Next steps:
250
- 1. Create login credentials: /portal-credentials <slug>
251
- 2. Preview the portal: /portal-preview <slug>
252
- 3. Edit content: /portal-update <slug>
286
+ 1. Preview the portal: /portal-preview <slug>
287
+ 2. Edit content: /portal-update <slug>
288
+ 3. Rotate credentials: /portal-credentials <slug>
253
289
  4. Deploy: /portal-deploy
254
290
  ```
255
291
 
256
- ### Step 10: Record learning
292
+ ### Step 11: Record learning
257
293
 
258
294
  Append a learning about the portal creation for future reference:
259
295
 
@@ -32,6 +32,10 @@ fi
32
32
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
33
33
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
34
34
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
35
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
36
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
37
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
38
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
35
39
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
36
40
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
37
41
  DATABASE_URL="${DATABASE_URL:-}"
@@ -72,6 +76,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
72
76
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
73
77
  echo '{"skill":"portal-credentials","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
74
78
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
79
+ if [ "portal-credentials" = "portal-deploy" ]; then
80
+ echo "ORG_SLUG: $ORG_SLUG"
81
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
82
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
83
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
84
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
85
+ fi
75
86
  echo "TELEMETRY: $TEL"
76
87
  echo "TEL_PROMPTED: $TEL_PROMPTED"
77
88
  ```
@@ -32,6 +32,10 @@ fi
32
32
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
33
33
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
34
34
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
35
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
36
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
37
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
38
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
35
39
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
36
40
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
37
41
  DATABASE_URL="${DATABASE_URL:-}"
@@ -72,6 +76,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
72
76
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
73
77
  echo '{"skill":"portal-delete","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
74
78
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
79
+ if [ "portal-delete" = "portal-deploy" ]; then
80
+ echo "ORG_SLUG: $ORG_SLUG"
81
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
82
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
83
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
84
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
85
+ fi
75
86
  echo "TELEMETRY: $TEL"
76
87
  echo "TEL_PROMPTED: $TEL_PROMPTED"
77
88
  ```
@@ -25,6 +25,10 @@ fi
25
25
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
26
26
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
27
27
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
28
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
29
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
30
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
31
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
28
32
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
29
33
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
30
34
  DATABASE_URL="${DATABASE_URL:-}"
@@ -65,6 +69,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
65
69
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
66
70
  echo '{"skill":"portal-deploy","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
67
71
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
72
+ if [ "portal-deploy" = "portal-deploy" ]; then
73
+ echo "ORG_SLUG: $ORG_SLUG"
74
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
75
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
76
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
77
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
78
+ fi
68
79
  echo "TELEMETRY: $TEL"
69
80
  echo "TEL_PROMPTED: $TEL_PROMPTED"
70
81
  ```
@@ -112,12 +123,26 @@ Check that the preamble successfully read cloud config values:
112
123
 
113
124
  ```bash
114
125
  if [ -z "$CLOUD_API_TOKEN" ]; then
115
- echo "ERROR: No API token found. Run 'showpane login' to authenticate."
126
+ echo "ERROR: No cloud access token found. Run 'showpane login' to authenticate."
116
127
  exit 1
117
128
  fi
118
129
  ```
119
130
 
120
- If the token is missing, stop and tell the user to run `showpane login`.
131
+ If the token is missing, stop and tell the user:
132
+
133
+ - `showpane login` is the right next step
134
+ - it opens browser auth for Showpane Cloud automatically
135
+ - if they need an account first:
136
+ - sign up: `https://app.showpane.com/sign-up`
137
+ - sign in: `https://app.showpane.com/sign-in`
138
+
139
+ If they are already inside Claude Code, explicitly suggest:
140
+
141
+ ```text
142
+ ! showpane login
143
+ ```
144
+
145
+ Then resume deploy after login finishes.
121
146
 
122
147
  #### 1b. Verify Showpane Cloud is reachable
123
148
 
@@ -28,12 +28,26 @@ Check that the preamble successfully read cloud config values:
28
28
 
29
29
  ```bash
30
30
  if [ -z "$CLOUD_API_TOKEN" ]; then
31
- echo "ERROR: No API token found. Run 'showpane login' to authenticate."
31
+ echo "ERROR: No cloud access token found. Run 'showpane login' to authenticate."
32
32
  exit 1
33
33
  fi
34
34
  ```
35
35
 
36
- If the token is missing, stop and tell the user to run `showpane login`.
36
+ If the token is missing, stop and tell the user:
37
+
38
+ - `showpane login` is the right next step
39
+ - it opens browser auth for Showpane Cloud automatically
40
+ - if they need an account first:
41
+ - sign up: `https://app.showpane.com/sign-up`
42
+ - sign in: `https://app.showpane.com/sign-in`
43
+
44
+ If they are already inside Claude Code, explicitly suggest:
45
+
46
+ ```text
47
+ ! showpane login
48
+ ```
49
+
50
+ Then resume deploy after login finishes.
37
51
 
38
52
  #### 1b. Verify Showpane Cloud is reachable
39
53
 
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-dev","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-dev" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-list","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-list" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -67,17 +67,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
67
67
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
68
68
  echo '{"skill":"portal-onboard","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
69
69
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE"
70
- echo "CONFIG_PRESENT: $CONFIG_PRESENT"
71
- echo "APP_PATH: ${APP_PATH:-missing}"
72
- echo "ORG_SLUG: ${ORG_SLUG:-missing}"
73
- echo "TELEMETRY: $TEL"
74
- echo "TEL_PROMPTED: $TEL_PROMPTED"
70
+ echo "WORKSPACE: ${APP_PATH:+ready}${APP_PATH:-missing}"
71
+ echo "ORG: ${ORG_SLUG:-missing}"
75
72
  if [ -f "$CHECKPOINT" ]; then
76
- echo "CHECKPOINT_PRESENT: true"
77
- echo "CHECKPOINT_DATA:"
73
+ echo "CHECKPOINT: present"
78
74
  cat "$CHECKPOINT"
79
75
  else
80
- echo "CHECKPOINT_PRESENT: false"
76
+ echo "CHECKPOINT: missing"
81
77
  fi
82
78
  ```
83
79
 
@@ -256,12 +252,33 @@ Save checkpoint with:
256
252
  Run the `portal-create` flow inline, but frame it as part of the wizard, not as
257
253
  a separate command.
258
254
 
255
+ Before you do, resolve the local organization with the canonical helper and keep
256
+ that result in hand:
257
+
258
+ ```bash
259
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/get-org.ts" --slug "$ORG_SLUG"
260
+ ```
261
+
262
+ Use the returned `org.id` as the canonical org id for the rest of the create flow.
263
+ Do not guess schema fields or discover the org via ad-hoc SQLite queries if this helper succeeds.
264
+
265
+ Also: do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or
266
+ template directories just to reorient yourself. At this point the canonical inputs
267
+ are already known:
268
+
269
+ - workspace app path
270
+ - org slug
271
+ - selected client company name
272
+ - selected template
273
+ - optional transcript source
274
+
259
275
  During this phase:
260
276
 
261
277
  - suggest a slug if needed
262
278
  - prefer a real company/client name in the portal
263
279
  - prefer useful structure over completeness
264
280
  - aim for a credible first draft, not a perfect final artifact
281
+ - do not re-ask for the company name or template if the user already chose them
265
282
 
266
283
  If the slug is taken, treat that as a soft retry, not a failure.
267
284
 
@@ -293,6 +310,16 @@ Run the `portal-preview` flow inline.
293
310
 
294
311
  If no dev server is running, start it using the `portal-dev` instructions first.
295
312
 
313
+ If the create step already generated local credentials, show them before opening
314
+ the preview link. Keep it simple:
315
+
316
+ - username
317
+ - password
318
+ - one sentence saying these are for local preview right now
319
+
320
+ Do this before opening the browser so the user is not dropped onto a login screen
321
+ without the credentials they need.
322
+
296
323
  Tell the user exactly what to inspect:
297
324
 
298
325
  - does the overall story feel right?
@@ -316,10 +343,14 @@ Recommended first-run choice:
316
343
  Important rule: share links still require portal credentials to exist, because
317
344
  share links are tied to credential versioning.
318
345
 
346
+ The `create-portal` flow currently creates initial credentials automatically. If
347
+ you already have a generated username/password from that step, do not immediately
348
+ re-run `portal-credentials` unless the user wants to rotate or replace them.
349
+
319
350
  So the flow is:
320
351
 
321
- 1. run `portal-credentials` inline
322
- 2. show the credentials once
352
+ 1. if credentials were already generated during create, show them once here
353
+ 2. if credentials do not exist or need changing, run `portal-credentials` inline
323
354
  3. explain that external sharing starts with `portal-deploy`
324
355
  4. if the user wants a direct hosted access link after publish, plan to run `portal-share`
325
356
 
@@ -159,12 +159,33 @@ Save checkpoint with:
159
159
  Run the `portal-create` flow inline, but frame it as part of the wizard, not as
160
160
  a separate command.
161
161
 
162
+ Before you do, resolve the local organization with the canonical helper and keep
163
+ that result in hand:
164
+
165
+ ```bash
166
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/get-org.ts" --slug "$ORG_SLUG"
167
+ ```
168
+
169
+ Use the returned `org.id` as the canonical org id for the rest of the create flow.
170
+ Do not guess schema fields or discover the org via ad-hoc SQLite queries if this helper succeeds.
171
+
172
+ Also: do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or
173
+ template directories just to reorient yourself. At this point the canonical inputs
174
+ are already known:
175
+
176
+ - workspace app path
177
+ - org slug
178
+ - selected client company name
179
+ - selected template
180
+ - optional transcript source
181
+
162
182
  During this phase:
163
183
 
164
184
  - suggest a slug if needed
165
185
  - prefer a real company/client name in the portal
166
186
  - prefer useful structure over completeness
167
187
  - aim for a credible first draft, not a perfect final artifact
188
+ - do not re-ask for the company name or template if the user already chose them
168
189
 
169
190
  If the slug is taken, treat that as a soft retry, not a failure.
170
191
 
@@ -196,6 +217,16 @@ Run the `portal-preview` flow inline.
196
217
 
197
218
  If no dev server is running, start it using the `portal-dev` instructions first.
198
219
 
220
+ If the create step already generated local credentials, show them before opening
221
+ the preview link. Keep it simple:
222
+
223
+ - username
224
+ - password
225
+ - one sentence saying these are for local preview right now
226
+
227
+ Do this before opening the browser so the user is not dropped onto a login screen
228
+ without the credentials they need.
229
+
199
230
  Tell the user exactly what to inspect:
200
231
 
201
232
  - does the overall story feel right?
@@ -219,10 +250,14 @@ Recommended first-run choice:
219
250
  Important rule: share links still require portal credentials to exist, because
220
251
  share links are tied to credential versioning.
221
252
 
253
+ The `create-portal` flow currently creates initial credentials automatically. If
254
+ you already have a generated username/password from that step, do not immediately
255
+ re-run `portal-credentials` unless the user wants to rotate or replace them.
256
+
222
257
  So the flow is:
223
258
 
224
- 1. run `portal-credentials` inline
225
- 2. show the credentials once
259
+ 1. if credentials were already generated during create, show them once here
260
+ 2. if credentials do not exist or need changing, run `portal-credentials` inline
226
261
  3. explain that external sharing starts with `portal-deploy`
227
262
  4. if the user wants a direct hosted access link after publish, plan to run `portal-share`
228
263
 
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-preview","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-preview" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -25,6 +25,10 @@ fi
25
25
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
26
26
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
27
27
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
28
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
29
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
30
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
31
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
28
32
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
29
33
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
30
34
  DATABASE_URL="${DATABASE_URL:-}"
@@ -65,6 +69,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
65
69
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
66
70
  echo '{"skill":"portal-share","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
67
71
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
72
+ if [ "portal-share" = "portal-deploy" ]; then
73
+ echo "ORG_SLUG: $ORG_SLUG"
74
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
75
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
76
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
77
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
78
+ fi
68
79
  echo "TELEMETRY: $TEL"
69
80
  echo "TEL_PROMPTED: $TEL_PROMPTED"
70
81
  ```
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-status","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-status" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-update","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-update" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-upgrade","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-upgrade" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
@@ -22,6 +22,10 @@ fi
22
22
  APP_PATH=$("$SHOWPANE_BIN/showpane-config" get app_path 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
23
23
  DEPLOY_MODE=$("$SHOWPANE_BIN/showpane-config" get deploy_mode 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null || echo "local")
24
24
  ORG_SLUG=$("$SHOWPANE_BIN/showpane-config" get orgSlug 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null || true)
25
+ CLOUD_API_TOKEN=$("$SHOWPANE_BIN/showpane-config" get accessToken 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('accessToken',''))" 2>/dev/null || true)
26
+ CLOUD_API_BASE="${SHOWPANE_CLOUD_URL:-https://app.showpane.com}"
27
+ CLOUD_ORG_SLUG="${ORG_SLUG:-}"
28
+ CLOUD_PORTAL_URL=$("$SHOWPANE_BIN/showpane-config" get portalUrl 2>/dev/null || python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('portalUrl',''))" 2>/dev/null || true)
25
29
  APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
26
30
  if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
27
31
  DATABASE_URL="${DATABASE_URL:-}"
@@ -62,6 +66,13 @@ SHOWPANE_TIMELINE="$SHOWPANE_HOME/timeline.jsonl"
62
66
  mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
63
67
  echo '{"skill":"portal-verify","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
64
68
  echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
69
+ if [ "portal-verify" = "portal-deploy" ]; then
70
+ echo "ORG_SLUG: $ORG_SLUG"
71
+ echo "CLOUD_API_TOKEN: ${CLOUD_API_TOKEN:+present}${CLOUD_API_TOKEN:-missing}"
72
+ echo "CLOUD_API_BASE: ${CLOUD_API_BASE:-missing}"
73
+ echo "CLOUD_ORG_SLUG: ${CLOUD_ORG_SLUG:-missing}"
74
+ echo "CLOUD_PORTAL_URL: ${CLOUD_PORTAL_URL:-missing}"
75
+ fi
65
76
  echo "TELEMETRY: $TEL"
66
77
  echo "TEL_PROMPTED: $TEL_PROMPTED"
67
78
  ```
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ var GREEN = "\x1B[32m";
32
32
  var BLUE = "\x1B[34m";
33
33
  var WHITE = "\x1B[37m";
34
34
  var RED = "\x1B[31m";
35
- var API_BASE = "https://app.showpane.com";
35
+ var API_BASE = process.env.SHOWPANE_CLOUD_URL || "https://app.showpane.com";
36
36
  var ORGANIZATION_REQUIRED_ERROR = "organization_required";
37
37
  var ORGANIZATION_NOT_READY_ERROR = "organization_not_ready";
38
38
  var SHOWPANE_HOME = join(homedir(), ".showpane");
@@ -62,10 +62,10 @@ function error(message) {
62
62
  console.error(` ${RED}\u2717${RESET} ${message}`);
63
63
  }
64
64
  function printCreateUsage() {
65
- console.log("Usage: showpane [--yes --name <company>] [--no-open] [--verbose]");
65
+ console.log("Usage: showpane [--yes --name <company> --full-name <name> --work-email <email> [--website <domain>]] [--no-open] [--verbose]");
66
66
  }
67
67
  function printClaudeUsage() {
68
- console.log("Usage: showpane claude [--project <name-or-path>] [--yes --name <company>] [--verbose]");
68
+ console.log("Usage: showpane claude [--project <name-or-path>] [--yes --name <company> --full-name <name> --work-email <email> [--website <domain>]] [--verbose]");
69
69
  }
70
70
  function printBanner() {
71
71
  const banner = `
@@ -120,10 +120,37 @@ function parseCreateArgs(args) {
120
120
  index += 1;
121
121
  continue;
122
122
  }
123
+ if (arg === "--full-name") {
124
+ const value = args[index + 1];
125
+ if (!value || value.startsWith("--")) {
126
+ throw new Error("Missing value for --full-name.");
127
+ }
128
+ options.contactName = value.trim();
129
+ index += 1;
130
+ continue;
131
+ }
132
+ if (arg === "--work-email") {
133
+ const value = args[index + 1];
134
+ if (!value || value.startsWith("--")) {
135
+ throw new Error("Missing value for --work-email.");
136
+ }
137
+ options.contactEmail = value.trim();
138
+ index += 1;
139
+ continue;
140
+ }
141
+ if (arg === "--website") {
142
+ const value = args[index + 1];
143
+ if (!value || value.startsWith("--")) {
144
+ throw new Error("Missing value for --website.");
145
+ }
146
+ options.websiteUrl = value.trim();
147
+ index += 1;
148
+ continue;
149
+ }
123
150
  throw new Error(`Unknown argument: ${arg}`);
124
151
  }
125
- if (options.yes && !options.companyName) {
126
- throw new Error("`--yes` requires `--name <company>` for a non-interactive install.");
152
+ if (options.yes && (!options.companyName || !options.contactName || !options.contactEmail)) {
153
+ throw new Error("`--yes` requires `--name <company> --full-name <name> --work-email <email>` for a non-interactive install.");
127
154
  }
128
155
  return options;
129
156
  }
@@ -314,7 +341,7 @@ async function maybeConfigureShellPath(config, options) {
314
341
  };
315
342
  }
316
343
  const answer = await ask(
317
- ` ${BOLD}Add Showpane to your PATH so 'showpane' works in future terminals?${RESET} ${DIM}(recommended) [Y/n]${RESET} `
344
+ ` ${DIM}(recommended)${RESET} ${BOLD}Want me to add Showpane to your PATH so \`showpane\` works in new terminals?${RESET} ${DIM}[Y/n]${RESET} `
318
345
  );
319
346
  if (answer && !["y", "yes"].includes(answer.toLowerCase())) {
320
347
  config.shellPathPrompted = true;
@@ -1257,9 +1284,9 @@ async function createProject(args) {
1257
1284
  blue("Let's get some basic info to set up your account.");
1258
1285
  console.log();
1259
1286
  }
1260
- const contactName = options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your full name?${RESET} `);
1261
- const contactEmail = options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your email?${RESET} `);
1262
- const websiteUrl = options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your company website or domain?${RESET} `);
1287
+ const contactName = options.contactName ?? (options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your full name?${RESET} `));
1288
+ const contactEmail = options.contactEmail ?? (options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your work email?${RESET} `));
1289
+ const websiteUrl = options.websiteUrl ?? (options.yes || !process.stdin.isTTY || !process.stdout.isTTY ? "" : await ask(` ${BOLD}What's your company website or domain?${RESET} `));
1263
1290
  const slug = toSlug(companyName);
1264
1291
  const dirName = `showpane-${slug}`;
1265
1292
  const projectRoot = resolve(process.cwd(), dirName);
@@ -1529,7 +1556,15 @@ async function login() {
1529
1556
  ensureShowpaneShim();
1530
1557
  blue("Authenticating with Showpane...");
1531
1558
  console.log();
1532
- const initRes = await fetch(`${API_BASE}/api/cli/init`, { method: "POST" });
1559
+ let initRes;
1560
+ try {
1561
+ initRes = await fetch(`${API_BASE}/api/cli/init`, { method: "POST" });
1562
+ } catch (errorLike) {
1563
+ const message = errorLike instanceof Error ? errorLike.message : String(errorLike);
1564
+ throw new Error(
1565
+ `Could not reach Showpane Cloud at ${API_BASE}. Check your internet connection, VPN/proxy settings, or try again in a moment. (${message})`
1566
+ );
1567
+ }
1533
1568
  if (!initRes.ok) {
1534
1569
  throw new Error(`Failed to start auth flow (${initRes.status})`);
1535
1570
  }
@@ -1589,12 +1624,21 @@ if (process.argv.includes("--version")) {
1589
1624
  console.log(getPackageVersion(packageRoot));
1590
1625
  process.exit(0);
1591
1626
  }
1627
+ if (process.argv.length <= 2 && process.argv.includes("--help")) {
1628
+ printCreateUsage();
1629
+ console.log("Commands: claude, login, projects, sync, upgrade");
1630
+ process.exit(0);
1631
+ }
1592
1632
  if (command === "login") {
1593
1633
  login().catch((err) => {
1594
1634
  error(String(err));
1595
1635
  process.exit(1);
1596
1636
  });
1597
1637
  } else if (command === "claude") {
1638
+ if (process.argv.includes("--help")) {
1639
+ printClaudeUsage();
1640
+ process.exit(0);
1641
+ }
1598
1642
  openClaude(process.argv.slice(3)).catch((err) => {
1599
1643
  error(String(err));
1600
1644
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.16",
3
+ "version": "0.4.18",
4
4
  "description": "CLI for Showpane — AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {