showpane 0.4.23 → 0.4.25

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.
@@ -10,6 +10,8 @@ type OrganizationNotReadyPayload = {
10
10
  orgSlug: string;
11
11
  reason: string;
12
12
  nextAction: string;
13
+ retryable: boolean;
14
+ retryAfterMs?: number;
13
15
  };
14
16
 
15
17
  type ShowpaneConfig = {
@@ -138,13 +138,8 @@ Store:
138
138
  If the helper fails, stop and tell the user to run `/portal-setup` again instead
139
139
  of guessing with ad-hoc SQL.
140
140
 
141
- Do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or template
142
- directories just to understand the project. Do not call `check-slug.ts` with
143
- anything except `--org-id`.
144
- Do not re-read config, Prisma, or SQLite to rediscover org fields that were already
145
- returned by `get-org.ts`.
146
-
147
- The canonical references for this skill are:
141
+ Once `get-org.ts` succeeds, extra project probing rarely improves the draft.
142
+ Use that result plus the selected template/example as the canonical references:
148
143
 
149
144
  - the configured `APP_PATH`
150
145
  - the configured `ORG_SLUG`
@@ -153,6 +148,9 @@ The canonical references for this skill are:
153
148
  - `$SKILL_DIR/templates/<chosen-template>/...`
154
149
  - `$APP_PATH/src/app/(portal)/client/example/example-client.tsx`
155
150
 
151
+ For slug checks, use `check-slug.ts` with `--org-id`. Re-reading config, Prisma,
152
+ or SQLite usually just burns time without changing the correct org context.
153
+
156
154
  ### Step 2: Determine the portal slug
157
155
 
158
156
  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.
@@ -215,10 +213,10 @@ Always also read the example portal as your quality and style reference:
215
213
  cat "$APP_PATH/src/app/(portal)/client/example/example-client.tsx"
216
214
  ```
217
215
 
218
- 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.
219
-
220
- Do not search the repo for templates or ask the filesystem where templates live.
221
- Use the selected template and the exact `SKILL_DIR` path above.
216
+ The template provides structure. The example provides quality and styling.
217
+ Read those directly from the known paths above, then match the example's card
218
+ styles, typography, spacing, and responsive breakpoints. Templates are
219
+ inspiration, not rigid scaffolds.
222
220
 
223
221
  ### Step 5: Analyze transcript (if available)
224
222
 
@@ -43,13 +43,8 @@ Store:
43
43
  If the helper fails, stop and tell the user to run `/portal-setup` again instead
44
44
  of guessing with ad-hoc SQL.
45
45
 
46
- Do not probe `showpane --help`, `package.json`, `scripts/`, `prisma/`, or template
47
- directories just to understand the project. Do not call `check-slug.ts` with
48
- anything except `--org-id`.
49
- Do not re-read config, Prisma, or SQLite to rediscover org fields that were already
50
- returned by `get-org.ts`.
51
-
52
- The canonical references for this skill are:
46
+ Once `get-org.ts` succeeds, extra project probing rarely improves the draft.
47
+ Use that result plus the selected template/example as the canonical references:
53
48
 
54
49
  - the configured `APP_PATH`
55
50
  - the configured `ORG_SLUG`
@@ -58,6 +53,9 @@ The canonical references for this skill are:
58
53
  - `$SKILL_DIR/templates/<chosen-template>/...`
59
54
  - `$APP_PATH/src/app/(portal)/client/example/example-client.tsx`
60
55
 
56
+ For slug checks, use `check-slug.ts` with `--org-id`. Re-reading config, Prisma,
57
+ or SQLite usually just burns time without changing the correct org context.
58
+
61
59
  ### Step 2: Determine the portal slug
62
60
 
63
61
  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.
@@ -120,10 +118,10 @@ Always also read the example portal as your quality and style reference:
120
118
  cat "$APP_PATH/src/app/(portal)/client/example/example-client.tsx"
121
119
  ```
122
120
 
123
- 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.
124
-
125
- Do not search the repo for templates or ask the filesystem where templates live.
126
- Use the selected template and the exact `SKILL_DIR` path above.
121
+ The template provides structure. The example provides quality and styling.
122
+ Read those directly from the known paths above, then match the example's card
123
+ styles, typography, spacing, and responsive breakpoints. Templates are
124
+ inspiration, not rigid scaffolds.
127
125
 
128
126
  ### Step 5: Analyze transcript (if available)
129
127
 
@@ -165,353 +165,65 @@ If type errors are found, display them and stop. Offer to fix simple issues (mis
165
165
 
166
166
  Run list-portals and warn about portals missing credentials. This is a warning, not a blocker.
167
167
 
168
- ### Cloud Step 2: Build the app
168
+ ### Cloud Step 2: Run the canonical deploy command
169
169
 
170
- Before building, ensure the hidden local project-link metadata exists:
170
+ Use the built-in deploy command instead of reimplementing the staged cloud protocol in shell.
171
171
 
172
172
  ```bash
173
- if [ ! -f "$APP_PATH/.vercel/project.json" ]; then
174
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/ensure-cloud-project-link.ts"
175
- fi
176
- ```
177
-
178
- Run the cloud build command that produces the prebuilt artifact:
179
-
180
- ```bash
181
- cd "$APP_PATH" && npm run cloud:build
182
- ```
183
-
184
- After the build completes, verify the output directory was created:
185
-
186
- ```bash
187
- ls -la "$APP_PATH/.vercel/output/"
188
- ```
189
-
190
- Expected: the `.vercel/output/` directory exists and contains `config.json`, `static/`, and optionally `functions/`. If the build fails or the output directory is missing, show the build errors and stop.
191
-
192
- The build typically takes 30-90 seconds depending on the number of portals.
193
-
194
- ### Cloud Step 3: Package the deploy artifact
195
-
196
- Package the prebuilt output plus the traced runtime files as a single zip artifact that can be handed to Showpane Cloud. This bundle must include:
197
- - `.vercel/output/**`
198
- - traced `.next/server/**` and `node_modules/**` files referenced by the build output
199
- - a sanitized `.env` stub so the traced runtime can resolve its expected path without leaking local secrets
200
-
201
- ```bash
202
- ARTIFACT_PATH="/tmp/showpane-deploy-${CLOUD_ORG_SLUG:-portal}.zip"
203
- rm -f "$ARTIFACT_PATH"
204
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/create-deploy-bundle.ts" --output "$ARTIFACT_PATH"
205
- test -f "$ARTIFACT_PATH" || { echo "ERROR: Artifact zip was not created"; exit 1; }
206
- echo "Artifact ready: $ARTIFACT_PATH"
207
- ```
208
-
209
- ### Cloud Step 4: Export runtime data
210
-
211
- Export the current local portal-runtime state so Showpane Cloud can sync credentials and portal metadata before publishing:
212
-
213
- ```bash
214
- RUNTIME_DATA_PATH="/tmp/showpane-runtime-${CLOUD_ORG_SLUG:-portal}.json"
215
- rm -f "$RUNTIME_DATA_PATH"
216
- 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" \
217
- || { echo "ERROR: Runtime payload export failed"; exit 1; }
218
- test -s "$RUNTIME_DATA_PATH" || { echo "ERROR: Runtime payload was not created"; exit 1; }
219
- echo "Runtime payload ready: $RUNTIME_DATA_PATH"
220
- ```
221
-
222
- ### Cloud Step 5: Export file manifest
223
-
224
- Export uploaded document metadata and checksums so Showpane Cloud can determine which files need syncing:
225
-
226
- ```bash
227
- FILE_MANIFEST_PATH="/tmp/showpane-files-${CLOUD_ORG_SLUG:-portal}.json"
228
- rm -f "$FILE_MANIFEST_PATH"
229
- 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" \
230
- || { echo "ERROR: File manifest export failed"; exit 1; }
231
- test -s "$FILE_MANIFEST_PATH" || { echo "ERROR: File manifest was not created"; exit 1; }
232
- echo "File manifest ready: $FILE_MANIFEST_PATH"
233
- ```
234
-
235
- ### Cloud Step 6: Sync uploaded files
236
-
237
- Ask Showpane Cloud which files are missing or stale, then upload only those file bytes:
238
-
239
- ```bash
240
- SYNC_PLAN_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/files/plan" \
241
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
242
- -H "Content-Type: application/json" \
243
- --data-binary @"$FILE_MANIFEST_PATH")
244
-
245
- MISSING_COUNT=$(echo "$SYNC_PLAN_RESPONSE" | python3 -c "
246
- import sys, json
247
- d = json.load(sys.stdin)
248
- if 'error' in d:
249
- print('ERROR: ' + d['error'])
250
- sys.exit(1)
251
- print(len(d.get('missing', [])))
252
- ")
253
-
254
- echo \"Files to sync: $MISSING_COUNT\"
255
-
256
- if [ \"$MISSING_COUNT\" -gt 0 ]; then
257
- TMP_SYNC_DIR=$(mktemp -d /tmp/showpane-file-sync.XXXXXX)
258
- echo \"$SYNC_PLAN_RESPONSE\" | python3 -c '
259
- import json, sys
260
- for item in json.load(sys.stdin).get(\"missing\", []):
261
- print(\"\\t\".join([
262
- item[\"storagePath\"],
263
- item[\"portalSlug\"],
264
- item[\"filename\"],
265
- item[\"mimeType\"],
266
- str(item[\"size\"]),
267
- item[\"uploadedBy\"],
268
- item[\"uploadedAt\"],
269
- item[\"checksum\"],
270
- ]))
271
- ' | while IFS=$'\t' read -r STORAGE_PATH PORTAL_SLUG FILE_NAME MIME_TYPE FILE_SIZE UPLOADED_BY UPLOADED_AT CHECKSUM; do
272
- TMP_FILE="$TMP_SYNC_DIR/$CHECKSUM"
273
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/materialize-file.ts" --storage-path "$STORAGE_PATH" --output "$TMP_FILE"
274
- curl -s -X POST "$CLOUD_API_BASE/api/files/upload" \
275
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
276
- -F "file=@$TMP_FILE;type=$MIME_TYPE" \
277
- -F "storagePath=$STORAGE_PATH" \
278
- -F "portalSlug=$PORTAL_SLUG" \
279
- -F "filename=$FILE_NAME" \
280
- -F "mimeType=$MIME_TYPE" \
281
- -F "size=$FILE_SIZE" \
282
- -F "uploadedBy=$UPLOADED_BY" \
283
- -F "uploadedAt=$UPLOADED_AT" \
284
- -F "checksum=$CHECKSUM" \
285
- >/dev/null || exit 1
286
- done
287
- fi
288
- ```
289
-
290
- ### Cloud Step 7: Start the staged deployment
291
-
292
- ```bash
293
- PORTAL_COUNT=$(cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" \
294
- | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('portals', [])))")
295
-
296
- INIT_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments" \
297
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
298
- -H "Content-Type: application/json" \
299
- --data-binary '{}')
300
-
301
- echo \"$INIT_RESPONSE\" | python3 -c "
302
- import sys, json
303
- d = json.load(sys.stdin)
304
- if 'error' in d:
305
- print('ERROR: ' + str(d['error']))
306
- sys.exit(1)
307
- print('Deployment initialized')
308
- print('ID: ' + d.get('deploymentId', 'unknown'))
309
- print('Status: ' + d.get('status', 'unknown'))
310
- "
173
+ DEPLOY_JSON=$(cd "$APP_PATH" && SHOWPANE_APP_PATH="$APP_PATH" NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$SKILL_DIR/bin/tsconfig.json" "$SKILL_DIR/bin/deploy-to-cloud.ts" --app-path "$APP_PATH" --wait --json)
174
+ echo "$DEPLOY_JSON"
311
175
  ```
312
176
 
313
- Extract the deployment ID and the presigned artifact upload URL from the response. If the API returns an error, show it and stop.
177
+ That command already owns:
178
+ - type check
179
+ - project-link bootstrap
180
+ - `cloud:build`
181
+ - artifact packaging
182
+ - runtime export
183
+ - optional file sync
184
+ - deployment init/upload/finalize
185
+ - polling to terminal state
186
+ - hosted verification
314
187
 
315
- ### Cloud Step 8: Upload the artifact directly to storage
188
+ Parse the JSON and stop immediately if `ok` is false.
316
189
 
317
- ```bash
318
- DEPLOY_ID=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('deploymentId',''))")
319
- ARTIFACT_UPLOAD_URL=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('artifactUploadUrl',''))")
320
-
321
- if [ -z "$DEPLOY_ID" ] || [ -z "$ARTIFACT_UPLOAD_URL" ]; then
322
- echo "ERROR: Missing deploymentId or artifact upload URL"
323
- exit 1
324
- fi
325
-
326
- UPLOAD_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$ARTIFACT_UPLOAD_URL" \
327
- -H "Content-Type: application/zip" \
328
- --data-binary @"$ARTIFACT_PATH" \
329
- )
330
-
331
- if [ "$UPLOAD_STATUS" != "200" ]; then
332
- echo "ERROR: Artifact upload failed with HTTP $UPLOAD_STATUS"
333
- exit 1
334
- fi
335
- ```
190
+ ### Cloud Step 3: Summarize the result
336
191
 
337
- ### Cloud Step 9: Finalize the deployment
338
-
339
- ```bash
340
- FINALIZE_PAYLOAD="/tmp/showpane-deploy-finalize-${DEPLOY_ID}.json"
341
- python3 - <<'PY' "$RUNTIME_DATA_PATH" "$PORTAL_COUNT" > "$FINALIZE_PAYLOAD"
342
- import json, sys
343
- runtime_path = sys.argv[1]
344
- portal_count = int(sys.argv[2])
345
- payload = {
346
- "portalCount": portal_count,
347
- "runtimeData": json.load(open(runtime_path)),
348
- }
349
- print(json.dumps(payload))
350
- PY
351
-
352
- FINALIZE_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID/finalize" \
353
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
354
- -H "Content-Type: application/json" \
355
- --data-binary @"$FINALIZE_PAYLOAD")
356
-
357
- echo "$FINALIZE_RESPONSE" | python3 -c "
358
- import sys, json
359
- d = json.load(sys.stdin)
360
- if 'error' in d:
361
- print('ERROR: ' + str(d['error']))
362
- sys.exit(1)
363
- print('Deployment accepted')
364
- print('ID: ' + d.get('deploymentId', 'unknown'))
365
- print('Status: ' + d.get('status', 'unknown'))
366
- "
367
- ```
192
+ Read these fields from the returned JSON:
193
+ - `deploymentId`
194
+ - `status`
195
+ - `liveUrl`
196
+ - `portalCount`
197
+ - `firstPortalSlug`
198
+ - `fileSyncCount`
199
+ - `verification.portalStatus`
200
+ - `verification.healthStatus`
368
201
 
369
- ### Cloud Step 10: Wait for cloud publish to finish
202
+ If `status` is not `live`, treat the deploy as failed.
370
203
 
371
- Poll Showpane Cloud until the deployment reaches `live` or `failed`:
204
+ Print a concise summary:
372
205
 
373
- ```bash
374
- echo "Waiting for deployment to go live..."
375
- for i in $(seq 1 60); do
376
- sleep 5
377
- STATUS=$(curl -s \
378
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
379
- "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID" \
380
- | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))")
381
-
382
- echo " Status: $STATUS"
383
-
384
- if [ "$STATUS" = "live" ]; then
385
- echo "Deployment is live!"
386
- break
387
- fi
388
-
389
- if [ "$STATUS" = "failed" ] || [ "$STATUS" = "unhealthy" ]; then
390
- echo "ERROR: Deployment failed with status: $STATUS"
391
- curl -s \
392
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
393
- "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID"
394
- exit 1
395
- fi
396
- done
397
-
398
- if [ "$STATUS" != "live" ]; then
399
- echo "WARNING: Deployment did not become ready within 5 minutes."
400
- echo "Check Showpane Cloud for status."
401
- fi
402
- ```
403
-
404
- The publish typically takes 15-60 seconds. Poll every 5 seconds for up to 5 minutes.
405
-
406
- ### Cloud Step 11: Post-deploy verification
407
-
408
- Once the deployment is live, verify the portal is accessible:
409
-
410
- ```bash
411
- PORTAL_URL="${CLOUD_PORTAL_URL:-https://$CLOUD_ORG_SLUG.showpane.com}"
412
-
413
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL")
414
- echo "Health check: $PORTAL_URL -> HTTP $HTTP_CODE"
415
- ```
416
-
417
- Expected: HTTP 200. If not 200, warn the user but do not treat it as a fatal error — DNS propagation or edge caching may cause a brief delay.
418
-
419
- Also check the API health endpoint:
420
-
421
- ```bash
422
- curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL/api/health"
423
- ```
424
-
425
- ### Cloud Step 12: Deployment summary
426
-
427
- Print a clear summary:
428
-
429
- ```
430
- Cloud deploy complete!
206
+ ```text
207
+ Cloud deploy complete
431
208
 
432
- Mode: cloud
433
- Type check: passed
434
- Build: .vercel/output/ (N files)
435
- Portals: N active
436
209
  Status: live
437
- Health: OK (200)
438
-
439
- Portal URL: https://orgslug.showpane.com
440
210
  Deploy ID: dep_xxxxxxxxxxxx
211
+ Live URL: https://orgslug.showpane.com
212
+ Portals: N
213
+ File sync: N
441
214
  ```
442
215
 
443
- ### Cloud Step 13: Record deployment
216
+ ### Cloud Step 4: Record deployment
444
217
 
445
218
  Log the cloud deployment for operational memory:
446
219
 
447
220
  ```bash
448
- 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"
449
- ```
450
-
451
- ### Cloud Step 14: Clean up
452
-
453
- Remove the temporary artifact:
454
-
455
- ```bash
456
- rm -f "$ARTIFACT_PATH"
457
- rm -f "$RUNTIME_DATA_PATH"
458
- rm -f "$FILE_MANIFEST_PATH"
459
- rm -f "$FINALIZE_PAYLOAD"
460
- [ -n "${TMP_SYNC_DIR:-}" ] && rm -rf "$TMP_SYNC_DIR"
221
+ DEPLOY_ID=$(echo "$DEPLOY_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('deploymentId',''))")
222
+ PORTAL_COUNT=$(echo "$DEPLOY_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('portalCount',0))")
223
+ echo '{"skill":"portal-deploy","key":"deploy","insight":"Cloud deploy to '$CLOUD_ORG_SLUG'.showpane.com. Portals: '$PORTAL_COUNT' active. Deploy ID: '$DEPLOY_ID'.","confidence":10,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
461
224
  ```
462
225
 
463
- ---
464
-
465
- ## Post-Deploy Verification
466
-
467
- After deployment succeeds, automatically run these verification steps. Do not ask the user — just do them.
468
-
469
- ### Step V1: DNS/URL Check
470
- Fetch the portal URL and verify it returns 200:
471
- ```bash
472
- PORTAL_URL="${CLOUD_PORTAL_URL:-https://$CLOUD_ORG_SLUG.showpane.com}"
473
- echo "Verifying $PORTAL_URL..."
474
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL" 2>/dev/null)
475
- echo "HTTP status: $HTTP_CODE"
476
- ```
477
- If not 200, warn but don't fail — DNS propagation may take time for cloud deploys.
478
-
479
- ### Step V2: Portal Content Check
480
- If portals exist in the database, verify at least one portal login page loads:
481
- ```bash
482
- # Get the first active portal slug
483
- FIRST_SLUG=$(cd "$APP_PATH" && npx tsx -e "
484
- const { prisma } = require('./src/lib/db');
485
- prisma.clientPortal.findFirst({ where: { isActive: true }, select: { slug: true } })
486
- .then(p => console.log(p?.slug || ''))
487
- .finally(() => prisma.\$disconnect());
488
- " 2>/dev/null)
489
- if [ -n "$FIRST_SLUG" ]; then
490
- echo "Checking portal: $PORTAL_URL/client/$FIRST_SLUG"
491
- curl -s -o /dev/null -w "Portal login page: %{http_code}\n" "$PORTAL_URL/client/$FIRST_SLUG" 2>/dev/null
492
- fi
493
- ```
494
-
495
- ### Step V3: SSL Check (cloud only)
496
- For cloud deploys, verify SSL is working:
497
- ```bash
498
- if [ "$DEPLOY_MODE" = "cloud" ] && [ -n "$CLOUD_PORTAL_URL" ]; then
499
- echo "Checking SSL..."
500
- SSL_OK=$(curl -s -o /dev/null -w "%{http_code}" "https://${CLOUD_ORG_SLUG}.showpane.com" 2>/dev/null)
501
- echo "SSL status: $SSL_OK"
502
- fi
503
- ```
504
-
505
- ### Step V4: Summary
506
- Print a deployment summary:
507
- ```
508
- ✓ Deployed successfully
509
- URL: [portal URL]
510
- Portals: [count]
511
- SSL: [ok/pending]
512
-
513
- Next: /portal-status for ongoing monitoring
514
- ```
226
+ The deploy command already handles cleanup and hosted verification. Do not duplicate those steps here.
515
227
 
516
228
  ---
517
229