showpane 0.4.11 → 0.4.13

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-04-09T22:32:17.805Z",
3
+ "generatedAt": "2026-04-09T23:30:19.010Z",
4
4
  "scaffoldVersion": "0.2.4",
5
5
  "files": {
6
6
  ".env.example": "ed105f2bdcd1888a98181d55e3c9f7d6eff3ae9c3e2366c2e777a12e3caddfa7",
@@ -19,7 +19,7 @@
19
19
  "scripts/prisma-db-push.mjs": "76ac85fe65b5dc3d9cc7432e44618fcc84b7443574c8d88198d01f13ac23c040",
20
20
  "scripts/prisma-generate.mjs": "d371e63388fa39f963b7c3c7cb8f87e0d9cd43cbf69d254b999108e29b8738c8",
21
21
  "scripts/prisma-schema.mjs": "0a86cc1b5f84120948aed8f97a84f2d5b173f91a43ea34ad6767441894121d83",
22
- "src/__tests__/client-portals.test.ts": "fe8e491e62fb2a84de52cdc1154d1451083f93bbccf1c5e65b42810d007eecc2",
22
+ "src/__tests__/client-portals.test.ts": "9c3236bf0f7190b7d5ba9082287dcb29bc00d28dd63782a89505125ead06c624",
23
23
  "src/__tests__/deploy-bundle.test.ts": "abd3216170f306c09df6abb0d2afad966a5741e8859f25a310a0a09693d37609",
24
24
  "src/__tests__/portal-contracts.test.ts": "80066377d3281786c2bb9ecc857514124e094a2e66dca2fb08ded994c25fa2bc",
25
25
  "src/app/(portal)/client/[slug]/page.tsx": "4f2f9253b2ad5d37a0f13759db52c786ae9c401f50fae9431da1417e9736e000",
@@ -16,7 +16,7 @@ vi.mock("@/lib/db", () => ({
16
16
  import { resolveDefaultOrganizationId } from "@/lib/client-portals";
17
17
  import { prisma } from "@/lib/db";
18
18
 
19
- const mockedPrisma = vi.mocked(prisma);
19
+ const mockedFindFirst = vi.mocked(prisma.organization.findFirst);
20
20
 
21
21
  describe("resolveDefaultOrganizationId", () => {
22
22
  beforeEach(() => {
@@ -24,21 +24,21 @@ describe("resolveDefaultOrganizationId", () => {
24
24
  });
25
25
 
26
26
  it("falls back to first org in DB", async () => {
27
- mockedPrisma.organization.findFirst.mockResolvedValue({
27
+ mockedFindFirst.mockResolvedValue({
28
28
  id: "local-org-1",
29
29
  } as never);
30
30
 
31
31
  const result = await resolveDefaultOrganizationId();
32
32
 
33
33
  expect(result).toBe("local-org-1");
34
- expect(mockedPrisma.organization.findFirst).toHaveBeenCalledWith({
34
+ expect(mockedFindFirst).toHaveBeenCalledWith({
35
35
  select: { id: true },
36
36
  orderBy: { createdAt: "asc" },
37
37
  });
38
38
  });
39
39
 
40
40
  it("returns null when no orgs exist in DB", async () => {
41
- mockedPrisma.organization.findFirst.mockResolvedValue(null);
41
+ mockedFindFirst.mockResolvedValue(null);
42
42
 
43
43
  const result = await resolveDefaultOrganizationId();
44
44
 
@@ -1,10 +1,97 @@
1
- import { createDeployBundle } from "../app/src/lib/deploy-bundle";
1
+ import AdmZip from "adm-zip";
2
+ import {
3
+ lstatSync,
4
+ readdirSync,
5
+ readFileSync,
6
+ realpathSync,
7
+ statSync,
8
+ } from "node:fs";
9
+ import path from "node:path";
2
10
 
3
11
  function fail(message: string): never {
4
12
  console.error(JSON.stringify({ ok: false, error: message }));
5
13
  process.exit(1);
6
14
  }
7
15
 
16
+ function walkFiles(dir: string, root: string, out: Set<string>) {
17
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
18
+ const fullPath = path.join(dir, entry.name);
19
+ if (entry.isDirectory()) {
20
+ walkFiles(fullPath, root, out);
21
+ continue;
22
+ }
23
+ out.add(path.relative(root, fullPath));
24
+ }
25
+ }
26
+
27
+ function collectTracedFiles(appPath: string): Set<string> {
28
+ const files = new Set<string>();
29
+ const outputRoot = path.join(appPath, ".vercel", "output");
30
+ const functionsRoot = path.join(outputRoot, "functions");
31
+
32
+ walkFiles(outputRoot, appPath, files);
33
+
34
+ const queue = [functionsRoot];
35
+ while (queue.length > 0) {
36
+ const current = queue.pop();
37
+ if (!current) continue;
38
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
39
+ const fullPath = path.join(current, entry.name);
40
+ if (entry.isDirectory()) {
41
+ queue.push(fullPath);
42
+ continue;
43
+ }
44
+ if (entry.name !== ".vc-config.json") continue;
45
+
46
+ const config = JSON.parse(readFileSync(fullPath, "utf8")) as {
47
+ filePathMap?: Record<string, string>;
48
+ };
49
+ for (const relativePath of Object.values(config.filePathMap ?? {})) {
50
+ files.add(relativePath);
51
+ }
52
+ }
53
+ }
54
+
55
+ return files;
56
+ }
57
+
58
+ function addPathToZip(zip: AdmZip, sourcePath: string, zipPath: string): number {
59
+ const zipPathPosix = zipPath.replace(/\\/g, "/");
60
+
61
+ if (zipPathPosix === ".env" || zipPathPosix.startsWith(".env.")) {
62
+ zip.addFile(zipPathPosix, Buffer.from("NODE_ENV=production\n"));
63
+ return 1;
64
+ }
65
+
66
+ const entry = lstatSync(sourcePath, { throwIfNoEntry: false });
67
+ if (!entry) {
68
+ return 0;
69
+ }
70
+
71
+ if (entry.isSymbolicLink()) {
72
+ return addPathToZip(zip, realpathSync(sourcePath), zipPathPosix);
73
+ }
74
+
75
+ if (entry.isDirectory()) {
76
+ let count = 0;
77
+ for (const child of readdirSync(sourcePath, { withFileTypes: true })) {
78
+ count += addPathToZip(
79
+ zip,
80
+ path.join(sourcePath, child.name),
81
+ path.posix.join(zipPathPosix, child.name),
82
+ );
83
+ }
84
+ return count;
85
+ }
86
+
87
+ if (!entry.isFile() && !statSync(sourcePath, { throwIfNoEntry: false })?.isFile()) {
88
+ return 0;
89
+ }
90
+
91
+ zip.addLocalFile(sourcePath, path.posix.dirname(zipPathPosix), path.posix.basename(zipPathPosix));
92
+ return 1;
93
+ }
94
+
8
95
  async function main() {
9
96
  const args = process.argv.slice(2);
10
97
  const outputIndex = args.indexOf("--output");
@@ -15,7 +102,20 @@ async function main() {
15
102
  }
16
103
 
17
104
  const appPath = process.cwd();
18
- const { fileCount } = createDeployBundle(appPath, outputPath);
105
+ const outputRoot = path.join(appPath, ".vercel", "output");
106
+ if (!statSync(outputRoot, { throwIfNoEntry: false })?.isDirectory()) {
107
+ fail("Missing .vercel/output. Run `npm run cloud:build` first.");
108
+ }
109
+
110
+ const zip = new AdmZip();
111
+ const tracedFiles = collectTracedFiles(appPath);
112
+ let fileCount = 0;
113
+
114
+ for (const relativePath of tracedFiles) {
115
+ fileCount += addPathToZip(zip, path.join(appPath, relativePath), relativePath);
116
+ }
117
+
118
+ zip.writeZip(outputPath);
19
119
  console.log(JSON.stringify({ ok: true, outputPath, fileCount }));
20
120
  }
21
121
 
@@ -110,7 +110,7 @@ Expected: HTTP 200. If Showpane Cloud is unreachable, stop and show the error. T
110
110
  Run the standard type check:
111
111
 
112
112
  ```bash
113
- cd "$APP_PATH" && npx tsc --noEmit 2>&1
113
+ cd "$APP_PATH" && npx tsc --noEmit
114
114
  ```
115
115
 
116
116
  If type errors are found, display them and stop. Offer to fix simple issues (missing imports, typos).
@@ -167,12 +167,9 @@ Export the current local portal-runtime state so Showpane Cloud can sync credent
167
167
  ```bash
168
168
  RUNTIME_DATA_PATH="/tmp/showpane-runtime-${CLOUD_ORG_SLUG:-portal}.json"
169
169
  rm -f "$RUNTIME_DATA_PATH"
170
- if [ -n "$ORG_SLUG" ]; then
171
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-runtime-state.ts" --org-slug "$ORG_SLUG" > "$RUNTIME_DATA_PATH"
172
- else
173
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-runtime-state.ts" > "$RUNTIME_DATA_PATH"
174
- fi
175
- test -f "$RUNTIME_DATA_PATH" || { echo "ERROR: Runtime payload was not created"; exit 1; }
170
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-runtime-state.ts" > "$RUNTIME_DATA_PATH" \
171
+ || { echo "ERROR: Runtime payload export failed"; exit 1; }
172
+ test -s "$RUNTIME_DATA_PATH" || { echo "ERROR: Runtime payload was not created"; exit 1; }
176
173
  echo "Runtime payload ready: $RUNTIME_DATA_PATH"
177
174
  ```
178
175
 
@@ -183,12 +180,9 @@ Export uploaded document metadata and checksums so Showpane Cloud can determine
183
180
  ```bash
184
181
  FILE_MANIFEST_PATH="/tmp/showpane-files-${CLOUD_ORG_SLUG:-portal}.json"
185
182
  rm -f "$FILE_MANIFEST_PATH"
186
- if [ -n "$ORG_SLUG" ]; then
187
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-file-manifest.ts" --org-slug "$ORG_SLUG" > "$FILE_MANIFEST_PATH"
188
- else
189
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-file-manifest.ts" > "$FILE_MANIFEST_PATH"
190
- fi
191
- test -f "$FILE_MANIFEST_PATH" || { echo "ERROR: File manifest was not created"; exit 1; }
183
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/export-file-manifest.ts" > "$FILE_MANIFEST_PATH" \
184
+ || { echo "ERROR: File manifest export failed"; exit 1; }
185
+ test -s "$FILE_MANIFEST_PATH" || { echo "ERROR: File manifest was not created"; exit 1; }
192
186
  echo "File manifest ready: $FILE_MANIFEST_PATH"
193
187
  ```
194
188
 
@@ -247,41 +241,90 @@ for item in json.load(sys.stdin).get(\"missing\", []):
247
241
  fi
248
242
  ```
249
243
 
250
- ### Cloud Step 7: Upload the artifact to Showpane Cloud
244
+ ### Cloud Step 7: Start the staged deployment
251
245
 
252
246
  ```bash
253
247
  PORTAL_COUNT=$(cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" \
254
248
  | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('portals', [])))")
255
249
 
256
- DEPLOY_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments" \
250
+ INIT_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments" \
257
251
  -H "Authorization: Bearer $CLOUD_API_TOKEN" \
258
- -F "artifact=@$ARTIFACT_PATH" \
259
- -F "source=claude-portal-deploy" \
260
- -F "app_path=$APP_PATH" \
261
- -F "portalCount=$PORTAL_COUNT" \
262
- -F "runtimeData=@$RUNTIME_DATA_PATH;type=application/json")
252
+ -H "Content-Type: application/json" \
253
+ --data-binary '{}')
263
254
 
264
- echo "$DEPLOY_RESPONSE" | python3 -c "
255
+ echo \"$INIT_RESPONSE\" | python3 -c "
265
256
  import sys, json
266
257
  d = json.load(sys.stdin)
267
258
  if 'error' in d:
268
259
  print('ERROR: ' + str(d['error']))
269
260
  sys.exit(1)
270
- print('Deployment accepted')
271
- print('ID: ' + d.get('deploymentId', d.get('id', 'unknown')))
261
+ print('Deployment initialized')
262
+ print('ID: ' + d.get('deploymentId', 'unknown'))
272
263
  print('Status: ' + d.get('status', 'unknown'))
273
264
  "
274
265
  ```
275
266
 
276
- Extract the deployment ID from the response. If the API returns an error, show it and stop.
267
+ Extract the deployment ID and the presigned artifact upload URL from the response. If the API returns an error, show it and stop.
277
268
 
278
- ### Cloud Step 8: Wait for cloud publish to finish
269
+ ### Cloud Step 8: Upload the artifact directly to storage
279
270
 
280
- Poll Showpane Cloud until the deployment reaches `live` or `failed`:
271
+ ```bash
272
+ DEPLOY_ID=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('deploymentId',''))")
273
+ ARTIFACT_UPLOAD_URL=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('artifactUploadUrl',''))")
274
+
275
+ if [ -z "$DEPLOY_ID" ] || [ -z "$ARTIFACT_UPLOAD_URL" ]; then
276
+ echo "ERROR: Missing deploymentId or artifact upload URL"
277
+ exit 1
278
+ fi
279
+
280
+ UPLOAD_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$ARTIFACT_UPLOAD_URL" \
281
+ -H "Content-Type: application/zip" \
282
+ --data-binary @"$ARTIFACT_PATH" \
283
+ )
284
+
285
+ if [ "$UPLOAD_STATUS" != "200" ]; then
286
+ echo "ERROR: Artifact upload failed with HTTP $UPLOAD_STATUS"
287
+ exit 1
288
+ fi
289
+ ```
290
+
291
+ ### Cloud Step 9: Finalize the deployment
281
292
 
282
293
  ```bash
283
- DEPLOY_ID=$(echo "$DEPLOY_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('deploymentId', d.get('id','')))")
294
+ FINALIZE_PAYLOAD="/tmp/showpane-deploy-finalize-${DEPLOY_ID}.json"
295
+ python3 - <<'PY' "$RUNTIME_DATA_PATH" "$PORTAL_COUNT" > "$FINALIZE_PAYLOAD"
296
+ import json, sys
297
+ runtime_path = sys.argv[1]
298
+ portal_count = int(sys.argv[2])
299
+ payload = {
300
+ "portalCount": portal_count,
301
+ "runtimeData": json.load(open(runtime_path)),
302
+ }
303
+ print(json.dumps(payload))
304
+ PY
305
+
306
+ FINALIZE_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID/finalize" \
307
+ -H "Authorization: Bearer $CLOUD_API_TOKEN" \
308
+ -H "Content-Type: application/json" \
309
+ --data-binary @"$FINALIZE_PAYLOAD")
310
+
311
+ echo "$FINALIZE_RESPONSE" | python3 -c "
312
+ import sys, json
313
+ d = json.load(sys.stdin)
314
+ if 'error' in d:
315
+ print('ERROR: ' + str(d['error']))
316
+ sys.exit(1)
317
+ print('Deployment accepted')
318
+ print('ID: ' + d.get('deploymentId', 'unknown'))
319
+ print('Status: ' + d.get('status', 'unknown'))
320
+ "
321
+ ```
284
322
 
323
+ ### Cloud Step 10: Wait for cloud publish to finish
324
+
325
+ Poll Showpane Cloud until the deployment reaches `live` or `failed`:
326
+
327
+ ```bash
285
328
  echo "Waiting for deployment to go live..."
286
329
  for i in $(seq 1 60); do
287
330
  sleep 5
@@ -314,7 +357,7 @@ fi
314
357
 
315
358
  The publish typically takes 15-60 seconds. Poll every 5 seconds for up to 5 minutes.
316
359
 
317
- ### Cloud Step 9: Post-deploy verification
360
+ ### Cloud Step 11: Post-deploy verification
318
361
 
319
362
  Once the deployment is live, verify the portal is accessible:
320
363
 
@@ -333,7 +376,7 @@ Also check the API health endpoint:
333
376
  curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL/api/health"
334
377
  ```
335
378
 
336
- ### Cloud Step 10: Deployment summary
379
+ ### Cloud Step 12: Deployment summary
337
380
 
338
381
  Print a clear summary:
339
382
 
@@ -351,7 +394,7 @@ Cloud deploy complete!
351
394
  Deploy ID: dep_xxxxxxxxxxxx
352
395
  ```
353
396
 
354
- ### Cloud Step 11: Record deployment
397
+ ### Cloud Step 13: Record deployment
355
398
 
356
399
  Log the cloud deployment for operational memory:
357
400
 
@@ -359,7 +402,7 @@ Log the cloud deployment for operational memory:
359
402
  echo '{"skill":"portal-deploy","key":"deploy","insight":"Cloud deploy to '$CLOUD_ORG_SLUG'.showpane.com. Migrations: <count>. Portals: <count> active. Deploy ID: '$DEPLOY_ID'.","confidence":10,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
360
403
  ```
361
404
 
362
- ### Cloud Step 12: Clean up
405
+ ### Cloud Step 14: Clean up
363
406
 
364
407
  Remove the temporary artifact:
365
408
 
@@ -367,6 +410,7 @@ Remove the temporary artifact:
367
410
  rm -f "$ARTIFACT_PATH"
368
411
  rm -f "$RUNTIME_DATA_PATH"
369
412
  rm -f "$FILE_MANIFEST_PATH"
413
+ rm -f "$FINALIZE_PAYLOAD"
370
414
  [ -n "${TMP_SYNC_DIR:-}" ] && rm -rf "$TMP_SYNC_DIR"
371
415
  ```
372
416
 
@@ -448,9 +492,9 @@ If the Showpane Cloud API returns 401/403 during pre-flight or publish:
448
492
  - **409 organization_required**: The user authenticated but has no org yet. Send them to Showpane Cloud checkout to start the trial, then re-run `showpane login`.
449
493
 
450
494
  ### Cloud: Artifact upload failure
451
- If the POST to `/api/deployments` fails:
452
- - **400 Bad Request**: The artifact is missing or malformed. Rebuild locally and retry.
453
- - **413 Payload Too Large**: The build artifact is too large. Remove oversized static assets from the portal app.
495
+ If the deployment init or finalize call fails:
496
+ - **400 Bad Request**: The runtime payload is missing or malformed. Rebuild locally and retry.
497
+ - **409 Conflict**: The deployment was not initialized, the artifact was not uploaded yet, or the org has no hosted project provisioned.
454
498
  - **422 Validation Error**: The cloud control plane rejected the deploy metadata. Show the response body directly.
455
499
 
456
500
  ### Cloud: File sync failure
@@ -481,7 +525,9 @@ echo '{"skill":"portal-deploy","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H
481
525
  - If any pre-flight check fails (type errors, missing deploy config), stop and explain
482
526
  - Show the full deployment summary with portal count, migration status, and health
483
527
  - If this is the first deploy, suggest running `/portal credentials` for all portals before deploying so clients can actually log in
528
+ - For Cloud deploys: use the local workspace's current SQLite org for runtime exports, not the cloud org slug from config
484
529
  - For Cloud deploys: build locally, upload the artifact to Showpane Cloud, and let the control plane publish it
530
+ - For Cloud deploys: upload the artifact directly to storage using the presigned URL, not through the deployment function body
485
531
  - For Cloud deploys: always wait for the deployment to reach `live` before declaring success
486
532
  - For Cloud deploys: the portal URL is `https://{org}.showpane.com` — verify it returns 200 after deploy
487
533
  - For Cloud deploys: clean up the temporary artifact after deploy completes
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.11",
4
- "description": "CLI for Showpane \u2014 AI-generated client portals",
3
+ "version": "0.4.13",
4
+ "description": "CLI for Showpane AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "showpane": "./dist/index.js"