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.
@@ -70,353 +70,65 @@ If type errors are found, display them and stop. Offer to fix simple issues (mis
70
70
 
71
71
  Run list-portals and warn about portals missing credentials. This is a warning, not a blocker.
72
72
 
73
- ### Cloud Step 2: Build the app
73
+ ### Cloud Step 2: Run the canonical deploy command
74
74
 
75
- Before building, ensure the hidden local project-link metadata exists:
75
+ Use the built-in deploy command instead of reimplementing the staged cloud protocol in shell.
76
76
 
77
77
  ```bash
78
- if [ ! -f "$APP_PATH/.vercel/project.json" ]; then
79
- cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/ensure-cloud-project-link.ts"
80
- fi
81
- ```
82
-
83
- Run the cloud build command that produces the prebuilt artifact:
84
-
85
- ```bash
86
- cd "$APP_PATH" && npm run cloud:build
87
- ```
88
-
89
- After the build completes, verify the output directory was created:
90
-
91
- ```bash
92
- ls -la "$APP_PATH/.vercel/output/"
93
- ```
94
-
95
- 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.
96
-
97
- The build typically takes 30-90 seconds depending on the number of portals.
98
-
99
- ### Cloud Step 3: Package the deploy artifact
100
-
101
- 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:
102
- - `.vercel/output/**`
103
- - traced `.next/server/**` and `node_modules/**` files referenced by the build output
104
- - a sanitized `.env` stub so the traced runtime can resolve its expected path without leaking local secrets
105
-
106
- ```bash
107
- ARTIFACT_PATH="/tmp/showpane-deploy-${CLOUD_ORG_SLUG:-portal}.zip"
108
- rm -f "$ARTIFACT_PATH"
109
- 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"
110
- test -f "$ARTIFACT_PATH" || { echo "ERROR: Artifact zip was not created"; exit 1; }
111
- echo "Artifact ready: $ARTIFACT_PATH"
112
- ```
113
-
114
- ### Cloud Step 4: Export runtime data
115
-
116
- Export the current local portal-runtime state so Showpane Cloud can sync credentials and portal metadata before publishing:
117
-
118
- ```bash
119
- RUNTIME_DATA_PATH="/tmp/showpane-runtime-${CLOUD_ORG_SLUG:-portal}.json"
120
- rm -f "$RUNTIME_DATA_PATH"
121
- 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" \
122
- || { echo "ERROR: Runtime payload export failed"; exit 1; }
123
- test -s "$RUNTIME_DATA_PATH" || { echo "ERROR: Runtime payload was not created"; exit 1; }
124
- echo "Runtime payload ready: $RUNTIME_DATA_PATH"
125
- ```
126
-
127
- ### Cloud Step 5: Export file manifest
128
-
129
- Export uploaded document metadata and checksums so Showpane Cloud can determine which files need syncing:
130
-
131
- ```bash
132
- FILE_MANIFEST_PATH="/tmp/showpane-files-${CLOUD_ORG_SLUG:-portal}.json"
133
- rm -f "$FILE_MANIFEST_PATH"
134
- 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" \
135
- || { echo "ERROR: File manifest export failed"; exit 1; }
136
- test -s "$FILE_MANIFEST_PATH" || { echo "ERROR: File manifest was not created"; exit 1; }
137
- echo "File manifest ready: $FILE_MANIFEST_PATH"
138
- ```
139
-
140
- ### Cloud Step 6: Sync uploaded files
141
-
142
- Ask Showpane Cloud which files are missing or stale, then upload only those file bytes:
143
-
144
- ```bash
145
- SYNC_PLAN_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/files/plan" \
146
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
147
- -H "Content-Type: application/json" \
148
- --data-binary @"$FILE_MANIFEST_PATH")
149
-
150
- MISSING_COUNT=$(echo "$SYNC_PLAN_RESPONSE" | python3 -c "
151
- import sys, json
152
- d = json.load(sys.stdin)
153
- if 'error' in d:
154
- print('ERROR: ' + d['error'])
155
- sys.exit(1)
156
- print(len(d.get('missing', [])))
157
- ")
158
-
159
- echo \"Files to sync: $MISSING_COUNT\"
160
-
161
- if [ \"$MISSING_COUNT\" -gt 0 ]; then
162
- TMP_SYNC_DIR=$(mktemp -d /tmp/showpane-file-sync.XXXXXX)
163
- echo \"$SYNC_PLAN_RESPONSE\" | python3 -c '
164
- import json, sys
165
- for item in json.load(sys.stdin).get(\"missing\", []):
166
- print(\"\\t\".join([
167
- item[\"storagePath\"],
168
- item[\"portalSlug\"],
169
- item[\"filename\"],
170
- item[\"mimeType\"],
171
- str(item[\"size\"]),
172
- item[\"uploadedBy\"],
173
- item[\"uploadedAt\"],
174
- item[\"checksum\"],
175
- ]))
176
- ' | while IFS=$'\t' read -r STORAGE_PATH PORTAL_SLUG FILE_NAME MIME_TYPE FILE_SIZE UPLOADED_BY UPLOADED_AT CHECKSUM; do
177
- TMP_FILE="$TMP_SYNC_DIR/$CHECKSUM"
178
- 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"
179
- curl -s -X POST "$CLOUD_API_BASE/api/files/upload" \
180
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
181
- -F "file=@$TMP_FILE;type=$MIME_TYPE" \
182
- -F "storagePath=$STORAGE_PATH" \
183
- -F "portalSlug=$PORTAL_SLUG" \
184
- -F "filename=$FILE_NAME" \
185
- -F "mimeType=$MIME_TYPE" \
186
- -F "size=$FILE_SIZE" \
187
- -F "uploadedBy=$UPLOADED_BY" \
188
- -F "uploadedAt=$UPLOADED_AT" \
189
- -F "checksum=$CHECKSUM" \
190
- >/dev/null || exit 1
191
- done
192
- fi
78
+ 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)
79
+ echo "$DEPLOY_JSON"
193
80
  ```
194
81
 
195
- ### Cloud Step 7: Start the staged deployment
82
+ That command already owns:
83
+ - type check
84
+ - project-link bootstrap
85
+ - `cloud:build`
86
+ - artifact packaging
87
+ - runtime export
88
+ - optional file sync
89
+ - deployment init/upload/finalize
90
+ - polling to terminal state
91
+ - hosted verification
196
92
 
197
- ```bash
198
- PORTAL_COUNT=$(cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" \
199
- | python3 -c "import sys,json; print(len(json.load(sys.stdin).get('portals', [])))")
200
-
201
- INIT_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments" \
202
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
203
- -H "Content-Type: application/json" \
204
- --data-binary '{}')
205
-
206
- echo \"$INIT_RESPONSE\" | python3 -c "
207
- import sys, json
208
- d = json.load(sys.stdin)
209
- if 'error' in d:
210
- print('ERROR: ' + str(d['error']))
211
- sys.exit(1)
212
- print('Deployment initialized')
213
- print('ID: ' + d.get('deploymentId', 'unknown'))
214
- print('Status: ' + d.get('status', 'unknown'))
215
- "
216
- ```
93
+ Parse the JSON and stop immediately if `ok` is false.
217
94
 
218
- Extract the deployment ID and the presigned artifact upload URL from the response. If the API returns an error, show it and stop.
95
+ ### Cloud Step 3: Summarize the result
219
96
 
220
- ### Cloud Step 8: Upload the artifact directly to storage
97
+ Read these fields from the returned JSON:
98
+ - `deploymentId`
99
+ - `status`
100
+ - `liveUrl`
101
+ - `portalCount`
102
+ - `firstPortalSlug`
103
+ - `fileSyncCount`
104
+ - `verification.portalStatus`
105
+ - `verification.healthStatus`
221
106
 
222
- ```bash
223
- DEPLOY_ID=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('deploymentId',''))")
224
- ARTIFACT_UPLOAD_URL=$(echo "$INIT_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('artifactUploadUrl',''))")
107
+ If `status` is not `live`, treat the deploy as failed.
225
108
 
226
- if [ -z "$DEPLOY_ID" ] || [ -z "$ARTIFACT_UPLOAD_URL" ]; then
227
- echo "ERROR: Missing deploymentId or artifact upload URL"
228
- exit 1
229
- fi
230
-
231
- UPLOAD_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$ARTIFACT_UPLOAD_URL" \
232
- -H "Content-Type: application/zip" \
233
- --data-binary @"$ARTIFACT_PATH" \
234
- )
235
-
236
- if [ "$UPLOAD_STATUS" != "200" ]; then
237
- echo "ERROR: Artifact upload failed with HTTP $UPLOAD_STATUS"
238
- exit 1
239
- fi
240
- ```
241
-
242
- ### Cloud Step 9: Finalize the deployment
243
-
244
- ```bash
245
- FINALIZE_PAYLOAD="/tmp/showpane-deploy-finalize-${DEPLOY_ID}.json"
246
- python3 - <<'PY' "$RUNTIME_DATA_PATH" "$PORTAL_COUNT" > "$FINALIZE_PAYLOAD"
247
- import json, sys
248
- runtime_path = sys.argv[1]
249
- portal_count = int(sys.argv[2])
250
- payload = {
251
- "portalCount": portal_count,
252
- "runtimeData": json.load(open(runtime_path)),
253
- }
254
- print(json.dumps(payload))
255
- PY
256
-
257
- FINALIZE_RESPONSE=$(curl -s -X POST "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID/finalize" \
258
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
259
- -H "Content-Type: application/json" \
260
- --data-binary @"$FINALIZE_PAYLOAD")
261
-
262
- echo "$FINALIZE_RESPONSE" | python3 -c "
263
- import sys, json
264
- d = json.load(sys.stdin)
265
- if 'error' in d:
266
- print('ERROR: ' + str(d['error']))
267
- sys.exit(1)
268
- print('Deployment accepted')
269
- print('ID: ' + d.get('deploymentId', 'unknown'))
270
- print('Status: ' + d.get('status', 'unknown'))
271
- "
272
- ```
109
+ Print a concise summary:
273
110
 
274
- ### Cloud Step 10: Wait for cloud publish to finish
275
-
276
- Poll Showpane Cloud until the deployment reaches `live` or `failed`:
277
-
278
- ```bash
279
- echo "Waiting for deployment to go live..."
280
- for i in $(seq 1 60); do
281
- sleep 5
282
- STATUS=$(curl -s \
283
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
284
- "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID" \
285
- | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('status','unknown'))")
286
-
287
- echo " Status: $STATUS"
288
-
289
- if [ "$STATUS" = "live" ]; then
290
- echo "Deployment is live!"
291
- break
292
- fi
293
-
294
- if [ "$STATUS" = "failed" ] || [ "$STATUS" = "unhealthy" ]; then
295
- echo "ERROR: Deployment failed with status: $STATUS"
296
- curl -s \
297
- -H "Authorization: Bearer $CLOUD_API_TOKEN" \
298
- "$CLOUD_API_BASE/api/deployments/$DEPLOY_ID"
299
- exit 1
300
- fi
301
- done
302
-
303
- if [ "$STATUS" != "live" ]; then
304
- echo "WARNING: Deployment did not become ready within 5 minutes."
305
- echo "Check Showpane Cloud for status."
306
- fi
307
- ```
308
-
309
- The publish typically takes 15-60 seconds. Poll every 5 seconds for up to 5 minutes.
310
-
311
- ### Cloud Step 11: Post-deploy verification
312
-
313
- Once the deployment is live, verify the portal is accessible:
314
-
315
- ```bash
316
- PORTAL_URL="${CLOUD_PORTAL_URL:-https://$CLOUD_ORG_SLUG.showpane.com}"
317
-
318
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL")
319
- echo "Health check: $PORTAL_URL -> HTTP $HTTP_CODE"
320
- ```
321
-
322
- 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.
323
-
324
- Also check the API health endpoint:
325
-
326
- ```bash
327
- curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL/api/health"
328
- ```
329
-
330
- ### Cloud Step 12: Deployment summary
331
-
332
- Print a clear summary:
333
-
334
- ```
335
- Cloud deploy complete!
111
+ ```text
112
+ Cloud deploy complete
336
113
 
337
- Mode: cloud
338
- Type check: passed
339
- Build: .vercel/output/ (N files)
340
- Portals: N active
341
114
  Status: live
342
- Health: OK (200)
343
-
344
- Portal URL: https://orgslug.showpane.com
345
115
  Deploy ID: dep_xxxxxxxxxxxx
116
+ Live URL: https://orgslug.showpane.com
117
+ Portals: N
118
+ File sync: N
346
119
  ```
347
120
 
348
- ### Cloud Step 13: Record deployment
121
+ ### Cloud Step 4: Record deployment
349
122
 
350
123
  Log the cloud deployment for operational memory:
351
124
 
352
125
  ```bash
353
- 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"
126
+ DEPLOY_ID=$(echo "$DEPLOY_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('deploymentId',''))")
127
+ PORTAL_COUNT=$(echo "$DEPLOY_JSON" | python3 -c "import sys,json; print(json.load(sys.stdin).get('portalCount',0))")
128
+ 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"
354
129
  ```
355
130
 
356
- ### Cloud Step 14: Clean up
357
-
358
- Remove the temporary artifact:
359
-
360
- ```bash
361
- rm -f "$ARTIFACT_PATH"
362
- rm -f "$RUNTIME_DATA_PATH"
363
- rm -f "$FILE_MANIFEST_PATH"
364
- rm -f "$FINALIZE_PAYLOAD"
365
- [ -n "${TMP_SYNC_DIR:-}" ] && rm -rf "$TMP_SYNC_DIR"
366
- ```
367
-
368
- ---
369
-
370
- ## Post-Deploy Verification
371
-
372
- After deployment succeeds, automatically run these verification steps. Do not ask the user — just do them.
373
-
374
- ### Step V1: DNS/URL Check
375
- Fetch the portal URL and verify it returns 200:
376
- ```bash
377
- PORTAL_URL="${CLOUD_PORTAL_URL:-https://$CLOUD_ORG_SLUG.showpane.com}"
378
- echo "Verifying $PORTAL_URL..."
379
- HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL" 2>/dev/null)
380
- echo "HTTP status: $HTTP_CODE"
381
- ```
382
- If not 200, warn but don't fail — DNS propagation may take time for cloud deploys.
383
-
384
- ### Step V2: Portal Content Check
385
- If portals exist in the database, verify at least one portal login page loads:
386
- ```bash
387
- # Get the first active portal slug
388
- FIRST_SLUG=$(cd "$APP_PATH" && npx tsx -e "
389
- const { prisma } = require('./src/lib/db');
390
- prisma.clientPortal.findFirst({ where: { isActive: true }, select: { slug: true } })
391
- .then(p => console.log(p?.slug || ''))
392
- .finally(() => prisma.\$disconnect());
393
- " 2>/dev/null)
394
- if [ -n "$FIRST_SLUG" ]; then
395
- echo "Checking portal: $PORTAL_URL/client/$FIRST_SLUG"
396
- curl -s -o /dev/null -w "Portal login page: %{http_code}\n" "$PORTAL_URL/client/$FIRST_SLUG" 2>/dev/null
397
- fi
398
- ```
399
-
400
- ### Step V3: SSL Check (cloud only)
401
- For cloud deploys, verify SSL is working:
402
- ```bash
403
- if [ "$DEPLOY_MODE" = "cloud" ] && [ -n "$CLOUD_PORTAL_URL" ]; then
404
- echo "Checking SSL..."
405
- SSL_OK=$(curl -s -o /dev/null -w "%{http_code}" "https://${CLOUD_ORG_SLUG}.showpane.com" 2>/dev/null)
406
- echo "SSL status: $SSL_OK"
407
- fi
408
- ```
409
-
410
- ### Step V4: Summary
411
- Print a deployment summary:
412
- ```
413
- ✓ Deployed successfully
414
- URL: [portal URL]
415
- Portals: [count]
416
- SSL: [ok/pending]
417
-
418
- Next: /portal-status for ongoing monitoring
419
- ```
131
+ The deploy command already handles cleanup and hosted verification. Do not duplicate those steps here.
420
132
 
421
133
  ---
422
134
 
@@ -262,9 +262,8 @@ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PA
262
262
  Use the returned `org.id` as the canonical org id for the rest of the create flow.
263
263
  Do not guess schema fields or discover the org via ad-hoc SQLite queries if this helper succeeds.
264
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:
265
+ At this point, extra repo reorientation rarely changes the first draft. Use the
266
+ known inputs below and spend the time shaping the portal instead:
268
267
 
269
268
  - workspace app path
270
269
  - org slug
@@ -272,6 +271,9 @@ are already known:
272
271
  - selected template
273
272
  - optional transcript source
274
273
 
274
+ If you need structure or style, read only the selected template and the example
275
+ portal. Other portal skills are for later phases and usually just add latency here.
276
+
275
277
  During this phase:
276
278
 
277
279
  - suggest a slug if needed
@@ -384,8 +386,10 @@ afterthought unless the user explicitly says they want to stop at local preview.
384
386
 
385
387
  If cloud auth is missing:
386
388
 
387
- - tell the user to run `showpane login`
388
- - after login completes, resume from the checkpoint
389
+ - run `showpane login` inline via Bash in the current session
390
+ - if you need to hand control to the user, tell them to run `! showpane login`
391
+ - let it handle sign-in, sign-up, and checkout if needed
392
+ - then resume publish from the checkpoint
389
393
 
390
394
  If deploy returns `organization_required`:
391
395
 
@@ -169,9 +169,8 @@ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PA
169
169
  Use the returned `org.id` as the canonical org id for the rest of the create flow.
170
170
  Do not guess schema fields or discover the org via ad-hoc SQLite queries if this helper succeeds.
171
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:
172
+ At this point, extra repo reorientation rarely changes the first draft. Use the
173
+ known inputs below and spend the time shaping the portal instead:
175
174
 
176
175
  - workspace app path
177
176
  - org slug
@@ -179,6 +178,9 @@ are already known:
179
178
  - selected template
180
179
  - optional transcript source
181
180
 
181
+ If you need structure or style, read only the selected template and the example
182
+ portal. Other portal skills are for later phases and usually just add latency here.
183
+
182
184
  During this phase:
183
185
 
184
186
  - suggest a slug if needed
@@ -291,8 +293,10 @@ afterthought unless the user explicitly says they want to stop at local preview.
291
293
 
292
294
  If cloud auth is missing:
293
295
 
294
- - tell the user to run `showpane login`
295
- - after login completes, resume from the checkpoint
296
+ - run `showpane login` inline via Bash in the current session
297
+ - if you need to hand control to the user, tell them to run `! showpane login`
298
+ - let it handle sign-in, sign-up, and checkout if needed
299
+ - then resume publish from the checkpoint
296
300
 
297
301
  If deploy returns `organization_required`:
298
302
 
package/dist/index.js CHANGED
@@ -67,6 +67,9 @@ function printCreateUsage() {
67
67
  function printClaudeUsage() {
68
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
+ function printDeployUsage() {
71
+ console.log("Usage: showpane deploy [--wait] [--json]");
72
+ }
70
73
  function printBanner() {
71
74
  const banner = `
72
75
  ${BOLD}${WHITE} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
@@ -736,6 +739,16 @@ function findFreePort(startPort) {
736
739
  function getPackageRoot() {
737
740
  return resolve(dirname(fileURLToPath(import.meta.url)), "..");
738
741
  }
742
+ function ensureCurrentToolchain(packageRoot2) {
743
+ const bundleRoot = getLocalBundleRoot(packageRoot2);
744
+ const showpaneVersion = getPackageVersion(packageRoot2);
745
+ const currentDeployScript = join(CURRENT_TOOLCHAIN_LINK, "bin", "deploy-to-cloud.ts");
746
+ const bundledDeployScript = join(bundleRoot, "toolchain", "bin", "deploy-to-cloud.ts");
747
+ if (existsSync(currentDeployScript) && existsSync(bundledDeployScript) && hashFile(currentDeployScript) === hashFile(bundledDeployScript)) {
748
+ return;
749
+ }
750
+ syncToolchain(bundleRoot, showpaneVersion, false);
751
+ }
739
752
  function getPackageVersion(packageRoot2) {
740
753
  const packageJson = readJson(join(packageRoot2, "package.json"));
741
754
  return packageJson.version;
@@ -1625,6 +1638,112 @@ async function login() {
1625
1638
  error("Authentication timed out. Please try again.");
1626
1639
  process.exit(1);
1627
1640
  }
1641
+ function parseDeployArgs(args) {
1642
+ for (const arg of args) {
1643
+ if (arg === "--wait" || arg === "--json" || arg === "--help") {
1644
+ continue;
1645
+ }
1646
+ throw new Error(`Unknown argument: ${arg}`);
1647
+ }
1648
+ return {
1649
+ wait: args.includes("--wait"),
1650
+ json: args.includes("--json"),
1651
+ help: args.includes("--help")
1652
+ };
1653
+ }
1654
+ async function deployProject(args) {
1655
+ const { wait, json, help } = parseDeployArgs(args);
1656
+ if (help) {
1657
+ printDeployUsage();
1658
+ process.exit(0);
1659
+ }
1660
+ const packageRoot2 = getPackageRoot();
1661
+ ensureCurrentToolchain(packageRoot2);
1662
+ ensureShowpaneShim();
1663
+ const config = readShowpaneConfig();
1664
+ const currentWorkspace = findWorkspaceRoot(process.cwd()) ?? (config.app_path ? findWorkspaceRoot(config.app_path) ?? resolve(config.app_path) : null);
1665
+ if (!currentWorkspace) {
1666
+ throw new Error("No Showpane workspace found. Run this inside a Showpane project, or set one up first.");
1667
+ }
1668
+ const scriptPath = join(CURRENT_TOOLCHAIN_LINK, "bin", "deploy-to-cloud.ts");
1669
+ if (!existsSync(scriptPath)) {
1670
+ throw new Error("Current Showpane toolchain is missing deploy-to-cloud.ts. Run `showpane sync`.");
1671
+ }
1672
+ const child = spawn(
1673
+ "npx",
1674
+ [
1675
+ "tsx",
1676
+ "--tsconfig",
1677
+ join(CURRENT_TOOLCHAIN_LINK, "bin", "tsconfig.json"),
1678
+ scriptPath,
1679
+ "--app-path",
1680
+ currentWorkspace,
1681
+ "--json",
1682
+ ...wait ? ["--wait"] : []
1683
+ ],
1684
+ {
1685
+ cwd: currentWorkspace,
1686
+ env: {
1687
+ ...process.env,
1688
+ SHOWPANE_APP_PATH: currentWorkspace,
1689
+ SHOWPANE_CLOUD_URL: API_BASE
1690
+ },
1691
+ stdio: ["ignore", "pipe", "inherit"]
1692
+ }
1693
+ );
1694
+ let stdout = "";
1695
+ child.stdout?.on("data", (chunk) => {
1696
+ stdout += chunk.toString();
1697
+ });
1698
+ const exitCode = await new Promise((resolveExit, rejectExit) => {
1699
+ child.on("error", rejectExit);
1700
+ child.on("close", (code) => resolveExit(code ?? 1));
1701
+ });
1702
+ let parsed = null;
1703
+ if (stdout.trim()) {
1704
+ try {
1705
+ parsed = JSON.parse(stdout.trim());
1706
+ } catch {
1707
+ parsed = null;
1708
+ }
1709
+ }
1710
+ if (exitCode !== 0 || !parsed || !parsed.ok) {
1711
+ if (json && parsed && !parsed.ok) {
1712
+ process.stdout.write(`${JSON.stringify(parsed)}
1713
+ `);
1714
+ process.exit(exitCode || 1);
1715
+ }
1716
+ const message = parsed && !parsed.ok ? parsed.step ? `${parsed.step}: ${parsed.error}` : parsed.error : "Cloud deploy failed.";
1717
+ throw new Error(message);
1718
+ }
1719
+ config.deploy_mode = "cloud";
1720
+ if (parsed.liveUrl) {
1721
+ config.portalUrl = parsed.liveUrl;
1722
+ }
1723
+ updateWorkspaceFromConfig(config, currentWorkspace, {
1724
+ name: basename(currentWorkspace),
1725
+ deployMode: "cloud",
1726
+ orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : ""
1727
+ });
1728
+ writeShowpaneConfig(config);
1729
+ if (json) {
1730
+ process.stdout.write(`${JSON.stringify(parsed)}
1731
+ `);
1732
+ return;
1733
+ }
1734
+ console.log();
1735
+ green("Cloud deploy finished");
1736
+ console.log(` ${BOLD}Deploy ID:${RESET} ${parsed.deploymentId}`);
1737
+ console.log(` ${BOLD}Status:${RESET} ${parsed.status}`);
1738
+ console.log(` ${BOLD}URL:${RESET} ${parsed.liveUrl ?? "pending"}`);
1739
+ console.log(` ${BOLD}Portals:${RESET} ${parsed.portalCount}`);
1740
+ console.log(` ${BOLD}File sync:${RESET} ${parsed.fileSyncCount}`);
1741
+ if (parsed.verification.portalStatus !== null || parsed.verification.healthStatus !== null) {
1742
+ console.log(
1743
+ ` ${BOLD}Verify:${RESET} portal=${parsed.verification.portalStatus ?? "n/a"} health=${parsed.verification.healthStatus ?? "n/a"}`
1744
+ );
1745
+ }
1746
+ }
1628
1747
  var command = process.argv[2];
1629
1748
  var packageRoot = getPackageRoot();
1630
1749
  if (process.argv.includes("--version")) {
@@ -1633,7 +1752,7 @@ if (process.argv.includes("--version")) {
1633
1752
  }
1634
1753
  if (process.argv.length <= 2 && process.argv.includes("--help")) {
1635
1754
  printCreateUsage();
1636
- console.log("Commands: claude, login, projects, sync, upgrade");
1755
+ console.log("Commands: claude, deploy, login, projects, sync, upgrade");
1637
1756
  process.exit(0);
1638
1757
  }
1639
1758
  if (command === "login") {
@@ -1657,6 +1776,11 @@ if (command === "login") {
1657
1776
  error(String(err));
1658
1777
  process.exit(1);
1659
1778
  }
1779
+ } else if (command === "deploy") {
1780
+ deployProject(process.argv.slice(3)).catch((err) => {
1781
+ error(String(err));
1782
+ process.exit(1);
1783
+ });
1660
1784
  } else if (command === "sync") {
1661
1785
  syncCurrentToolchain().catch((err) => {
1662
1786
  error(String(err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "showpane",
3
- "version": "0.4.23",
3
+ "version": "0.4.25",
4
4
  "description": "CLI for Showpane — AI-generated client portals",
5
5
  "type": "module",
6
6
  "bin": {