showpane 0.4.0 → 0.4.2

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 (106) hide show
  1. package/README.md +22 -1
  2. package/bundle/meta/scaffold-manifest.json +73 -0
  3. package/bundle/scaffold/VERSION +1 -0
  4. package/bundle/scaffold/__dot__env.example +24 -0
  5. package/bundle/scaffold/__dot__gitignore +41 -0
  6. package/bundle/scaffold/docker/Caddyfile +3 -0
  7. package/bundle/scaffold/docker/Dockerfile +30 -0
  8. package/bundle/scaffold/docker-compose.yml +53 -0
  9. package/bundle/scaffold/next.config.ts +20 -0
  10. package/bundle/scaffold/package-lock.json +5843 -0
  11. package/bundle/scaffold/package.json +42 -0
  12. package/bundle/scaffold/postcss.config.js +6 -0
  13. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
  14. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
  15. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
  16. package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
  17. package/bundle/scaffold/prisma/schema.local.prisma +131 -0
  18. package/bundle/scaffold/prisma/schema.prisma +128 -0
  19. package/bundle/scaffold/prisma/seed.ts +49 -0
  20. package/bundle/scaffold/public/example-avatar.svg +4 -0
  21. package/bundle/scaffold/public/example-logo.svg +4 -0
  22. package/bundle/scaffold/public/robots.txt +2 -0
  23. package/bundle/scaffold/scripts/backup.sh +19 -0
  24. package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
  25. package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
  26. package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
  27. package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
  28. package/bundle/scaffold/scripts/restore.sh +31 -0
  29. package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
  30. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
  31. package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
  32. package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
  33. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
  34. package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
  35. package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
  36. package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
  37. package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
  38. package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
  39. package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
  40. package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
  41. package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
  42. package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
  43. package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
  44. package/bundle/scaffold/src/app/api/health/route.ts +19 -0
  45. package/bundle/scaffold/src/app/globals.css +7 -0
  46. package/bundle/scaffold/src/app/layout.tsx +25 -0
  47. package/bundle/scaffold/src/app/page.tsx +171 -0
  48. package/bundle/scaffold/src/components/portal-login.tsx +169 -0
  49. package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
  50. package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
  51. package/bundle/scaffold/src/lib/branding.ts +50 -0
  52. package/bundle/scaffold/src/lib/client-auth.ts +98 -0
  53. package/bundle/scaffold/src/lib/client-portals.ts +134 -0
  54. package/bundle/scaffold/src/lib/control-plane.ts +100 -0
  55. package/bundle/scaffold/src/lib/db.ts +7 -0
  56. package/bundle/scaffold/src/lib/files.ts +124 -0
  57. package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
  58. package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
  59. package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
  60. package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
  61. package/bundle/scaffold/src/lib/storage.ts +204 -0
  62. package/bundle/scaffold/src/lib/token.ts +186 -0
  63. package/bundle/scaffold/src/lib/utils.ts +6 -0
  64. package/bundle/scaffold/src/middleware.ts +61 -0
  65. package/bundle/scaffold/tailwind.config.ts +15 -0
  66. package/bundle/scaffold/tests/__dot__gitkeep +0 -0
  67. package/bundle/scaffold/tsconfig.json +23 -0
  68. package/bundle/scaffold/vitest.config.ts +13 -0
  69. package/bundle/toolchain/VERSION +1 -0
  70. package/bundle/toolchain/bin/check-slug.ts +59 -0
  71. package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
  72. package/bundle/toolchain/bin/create-portal.ts +71 -0
  73. package/bundle/toolchain/bin/delete-portal.ts +48 -0
  74. package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
  75. package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
  76. package/bundle/toolchain/bin/generate-share-link.ts +68 -0
  77. package/bundle/toolchain/bin/list-portals.ts +53 -0
  78. package/bundle/toolchain/bin/materialize-file.ts +35 -0
  79. package/bundle/toolchain/bin/query-analytics.ts +88 -0
  80. package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
  81. package/bundle/toolchain/bin/showpane-config +63 -0
  82. package/bundle/toolchain/bin/tsconfig.json +13 -0
  83. package/bundle/toolchain/skills/VERSION +1 -0
  84. package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
  85. package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
  86. package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
  87. package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
  88. package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
  89. package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
  90. package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
  91. package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
  92. package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
  93. package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
  94. package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
  95. package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
  96. package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
  97. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
  98. package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
  99. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
  100. package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
  101. package/bundle/toolchain/skills/shared/preamble.md +137 -0
  102. package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
  103. package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
  104. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
  105. package/dist/index.js +875 -159
  106. package/package.json +4 -2
@@ -0,0 +1,274 @@
1
+ ---
2
+ name: portal-credentials
3
+ description: |
4
+ Create or rotate login credentials for a client portal. Generates username and password, hashes and stores in DB.
5
+ Trigger phrases: "portal credentials", "create login", "rotate password", "reset credentials", "portal password". (showpane)
6
+ allowed-tools: [Bash, Read, Write, Edit]
7
+ hooks:
8
+ PreToolUse:
9
+ - matcher: "Bash"
10
+ hooks:
11
+ - type: command
12
+ command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
13
+ - matcher: "Edit"
14
+ hooks:
15
+ - type: command
16
+ command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
17
+ ---
18
+
19
+ ## Preamble (run first)
20
+
21
+ ```bash
22
+ # Read config
23
+ CONFIG="$HOME/.showpane/config.json"
24
+ if [ ! -f "$CONFIG" ]; then
25
+ echo "Showpane not configured. Run /portal setup first."
26
+ exit 1
27
+ fi
28
+ APP_PATH=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
29
+ DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','docker'))" 2>/dev/null)
30
+ ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
31
+ APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
32
+ if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
33
+ DATABASE_URL="${DATABASE_URL:-}"
34
+ if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
35
+ echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
36
+ exit 1
37
+ fi
38
+ SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
39
+ SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
40
+ echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
41
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
42
+ [ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
43
+
44
+ # Predictive next-skill suggestion
45
+ if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
46
+ _RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
47
+ [ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
48
+ fi
49
+
50
+ # Search relevant learnings
51
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
52
+ if [ -f "$LEARN_FILE" ]; then
53
+ _LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
54
+ echo "LEARNINGS: $_LEARN_COUNT entries"
55
+ if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
56
+ echo "RECENT_LEARNINGS:"
57
+ tail -5 "$LEARN_FILE" 2>/dev/null
58
+ fi
59
+ fi
60
+
61
+ # Track skill execution
62
+ SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
63
+ mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
64
+ echo '{"skill":"portal-credentials","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
65
+ ```
66
+
67
+ If RECENT_SKILLS is shown, suggest the likely next skill:
68
+ - After portal-create → suggest /portal-preview
69
+ - After portal-preview → suggest /portal-deploy or /portal-share
70
+ - After portal-deploy → suggest /portal-status or /portal-verify
71
+ - After portal-setup → suggest /portal-create
72
+ - After portal-credentials → suggest /portal-share
73
+ - After portal-update → suggest /portal-deploy
74
+
75
+ If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
76
+ relevant warnings or tips for this operation. Apply them where relevant but don't
77
+ mention them unless they directly affect the current task.
78
+
79
+ ## Steps
80
+
81
+ ### Step 1: Identify the portal
82
+
83
+ If the user provided a slug (e.g., `/portal credentials acme-health`), use it. Otherwise, list available portals to help the user choose:
84
+
85
+ ```bash
86
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/list-portals.ts" --org-id <org_id>
87
+ ```
88
+
89
+ Present the list and ask which portal needs credentials. If there is only one portal, confirm it rather than asking.
90
+
91
+ Verify the portal exists by checking the database. The `create-portal.ts` script should have been run during `/portal create` to register the portal. If no DB record exists, inform the user:
92
+
93
+ > "No portal record found for '<slug>'. Run `/portal create <slug>` first to register it."
94
+
95
+ Also check the portal's current credential status from the list output. If credentials already exist, inform the user before proceeding:
96
+
97
+ > "Portal '<slug>' already has credentials (username: <username>). Running this will rotate to a new password and invalidate all existing sessions. Continue? (y/N)"
98
+
99
+ This confirmation is important because credential rotation has an immediate impact on any clients currently logged in. Default to "no" to prevent accidental rotation.
100
+
101
+ ### Step 2: Run the rotate-credentials script
102
+
103
+ This script handles both initial credential creation and rotation:
104
+
105
+ ```bash
106
+ cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/rotate-credentials.ts" --slug <slug> --org-id <org_id>
107
+ ```
108
+
109
+ The script returns JSON on stdout:
110
+
111
+ **For initial creation:**
112
+ ```json
113
+ {"ok": true, "username": "acme-health", "password": "xK9mP2vL8nQr", "rotated": false}
114
+ ```
115
+
116
+ **For rotation (credentials already existed):**
117
+ ```json
118
+ {"ok": true, "username": "acme-health", "password": "bT4wN7jF3hYs", "rotated": true}
119
+ ```
120
+
121
+ **On error:**
122
+ ```json
123
+ {"error": "portal_not_found", "message": "No portal with slug 'acme-health' in org 'demo'"}
124
+ ```
125
+
126
+ ### Step 3: Handle script errors
127
+
128
+ If the script returns an error, handle it based on the error type:
129
+
130
+ - `portal_not_found` — The portal slug doesn't exist in the database for this org. Suggest running `/portal create <slug>` first.
131
+ - `database_error` — Connection or query failure. Check DATABASE_URL, verify the database is running, and check that migrations are up to date (`cd $APP_PATH && npx prisma migrate deploy`).
132
+ - `auth_secret_missing` — The AUTH_SECRET environment variable is not set in `$APP_PATH/.env`. The rotate-credentials script needs this to function. Suggest adding `AUTH_SECRET=<random-string>` to the .env file.
133
+
134
+ If the script exits with a non-zero code but produces no JSON output, check stderr for Prisma or Node.js errors and relay them to the user.
135
+
136
+ ### Step 4: Display credentials
137
+
138
+ Present the credentials clearly and prominently. Use an ASCII box for visibility:
139
+
140
+ ```
141
+ ┌─────────────────────────────────────────┐
142
+ │ Portal credentials for: acme-health │
143
+ │ │
144
+ │ Username: acme-health │
145
+ │ Password: xK9mP2vL8nQr │
146
+ │ │
147
+ │ Login URL: /client │
148
+ └─────────────────────────────────────────┘
149
+ ```
150
+
151
+ The ASCII box ensures the credentials stand out clearly in terminal output. Do not use markdown formatting (bold, code blocks) as these may not render in all terminal environments.
152
+
153
+ ### Step 5: Security warning
154
+
155
+ Immediately after displaying credentials, show this warning:
156
+
157
+ > **Save these credentials now.** They will not be shown again. The password is hashed in the database and cannot be retrieved. If lost, run `/portal credentials <slug>` again to rotate to a new password.
158
+
159
+ If this was a rotation (`"rotated": true`), add:
160
+
161
+ > **Credential rotation complete.** All existing sessions for this portal have been invalidated immediately. Anyone currently logged in will need to re-authenticate with the new password.
162
+
163
+ ### Step 6: Suggest sharing method
164
+
165
+ Provide guidance on how to share the credentials with the client:
166
+
167
+ > **How to share with your client:**
168
+ > - Send the username and password via a secure channel (encrypted email, Signal, etc.)
169
+ > - The login page is at `/client` — the client enters the portal slug as the "company" field, then the username and password
170
+ > - For a direct link without credentials, use `/portal share <slug>` to generate a reusable share URL that bypasses the login entirely
171
+
172
+ Recommend against sharing credentials via:
173
+ - Unencrypted email (can be intercepted)
174
+ - Slack or Teams messages (persistent and searchable by admins)
175
+ - Shared documents or spreadsheets
176
+
177
+ Good sharing channels:
178
+ - Signal or WhatsApp (encrypted, ephemeral)
179
+ - A phone call (verbal)
180
+ - An encrypted email service
181
+ - A password manager's secure sharing feature
182
+
183
+ ### Step 7: Verify login works (if dev server running)
184
+
185
+ If the dev server is running, suggest testing the credentials:
186
+
187
+ ```bash
188
+ lsof -i :3000 -sTCP:LISTEN -t 2>/dev/null
189
+ ```
190
+
191
+ If running, inform the user:
192
+
193
+ > "Test the login at http://localhost:3000/client — enter slug '<slug>', username '<username>', and the password shown above."
194
+
195
+ This step is optional but helps catch configuration issues (like AUTH_SECRET not being set) before sharing credentials with a client.
196
+
197
+ ### Step 8: Summary
198
+
199
+ Print a brief summary:
200
+
201
+ ```
202
+ Credentials created for: acme-health
203
+ Action: new credentials (or: rotated — old sessions invalidated)
204
+ Username: acme-health
205
+ Login: /client
206
+
207
+ Next steps:
208
+ 1. Send credentials to the client via a secure channel
209
+ 2. Generate a share link: /portal share <slug>
210
+ 3. Deploy if not already live: /portal deploy
211
+ ```
212
+
213
+ ### Step 9: Record credential event
214
+
215
+ Record that credentials were created or rotated (but NEVER record the actual credentials):
216
+
217
+ ```bash
218
+ echo '{"skill":"portal-credentials","key":"credentials-created","insight":"Created credentials for <slug>. Rotated: <true|false>.","confidence":10,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
219
+ ```
220
+
221
+ ## Security Conventions
222
+
223
+ - **Credentials are shown exactly once.** Never log them, never write them to a file, never include them in learnings or telemetry.
224
+ - **Rotation is immediate.** When credentials are rotated, the `credentialVersion` is bumped in the database. All existing session tokens for that portal are invalidated instantly because they reference the old version.
225
+ - **Passwords are generated server-side** by the `rotate-credentials.ts` script using cryptographically secure random bytes. Do not generate passwords in the skill — always use the script.
226
+ - **Usernames are derived from the slug.** The username is typically the same as the portal slug (e.g., slug `acme-health` gets username `acme-health`).
227
+ - **No password in config or learnings.** The `~/.showpane/config.json` and `~/.showpane/learnings.jsonl` files must never contain passwords or password hashes.
228
+ - **AUTH_SECRET stays in .env.** The secret used for token signing lives in `$APP_PATH/.env` and is read at runtime by the app. It is never copied to config.json.
229
+
230
+ ## Error Handling
231
+
232
+ - **Script not found**: If `$SKILL_DIR/bin/rotate-credentials.ts` does not exist, the skill pack may not be fully installed. Suggest running `/portal upgrade` or checking the Showpane installation.
233
+ - **Prisma connection error**: DATABASE_URL may be wrong or the database may be down. Check the .env file and database status.
234
+ - **Permission denied on .env**: The preamble sources `$APP_PATH/.env`. If this file is not readable, the DATABASE_URL will not be set. Check file permissions.
235
+ - **Script timeout**: If the script takes more than 30 seconds, something is wrong. The most common cause is a database connection timeout — check network connectivity to the database host.
236
+
237
+ ## Bulk credential creation
238
+
239
+ If the user asks to create credentials for multiple portals at once, run the rotate-credentials script for each portal sequentially. Display all credential sets together in a single output block:
240
+
241
+ ```
242
+ ┌─────────────────────────────────────────┐
243
+ │ 1. acme-health │
244
+ │ Username: acme-health │
245
+ │ Password: xK9mP2vL8nQr │
246
+ │ │
247
+ │ 2. beta-corp │
248
+ │ Username: beta-corp │
249
+ │ Password: mT7kR4wQ9nFs │
250
+ └─────────────────────────────────────────┘
251
+ ```
252
+
253
+ Even in bulk mode, show the security warning once after all credentials are displayed.
254
+
255
+ ## Completion
256
+
257
+ As a final step, log skill completion:
258
+
259
+ ```bash
260
+ echo '{"skill":"portal-credentials","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
261
+ ```
262
+
263
+ ## Conventions
264
+
265
+ - Always run the bin script rather than manipulating the database directly — never generate passwords, hash them, or write to the database from the skill
266
+ - Show credentials in an ASCII box for clear visibility in the terminal
267
+ - Warn about credential impermanence immediately after displaying them
268
+ - If the portal has no DB record, direct the user to `/portal create` first
269
+ - Never store, log, or record the plaintext password anywhere — not in learnings, not in telemetry, not in config
270
+ - Confirm before rotating existing credentials (default to "no") since rotation invalidates sessions
271
+ - The username is always the same as the portal slug — this is by design for simplicity
272
+ - If the user asks "what is the password for X portal", explain that passwords are one-way hashed and cannot be retrieved — offer to rotate to a new one instead
273
+ - After creating credentials, always suggest the next step: either share with the client or deploy if not already live
274
+ - If the app is not deployed yet, credentials still work — they are stored in the database regardless of whether the app is running in production
@@ -0,0 +1,265 @@
1
+ ---
2
+ name: portal-delete
3
+ description: |
4
+ Deactivate a client portal. Use when asked to "delete portal", "remove portal",
5
+ "deactivate portal", "archive portal", or "shut down portal". (showpane)
6
+ allowed-tools: [Bash, Read, Write, Edit]
7
+ hooks:
8
+ PreToolUse:
9
+ - matcher: "Bash"
10
+ hooks:
11
+ - type: command
12
+ command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
13
+ - matcher: "Edit"
14
+ hooks:
15
+ - type: command
16
+ command: "bash ${CLAUDE_SKILL_DIR}/../showpane-shared/bin/check-portal-guard.sh"
17
+ ---
18
+
19
+ ## Preamble (run first)
20
+
21
+ Before doing anything else, execute this block in a Bash tool call:
22
+
23
+ ```bash
24
+ CONFIG="$HOME/.showpane/config.json"
25
+ if [ ! -f "$CONFIG" ]; then
26
+ echo "Showpane not configured. Run /portal setup first."
27
+ exit 1
28
+ fi
29
+ APP_PATH=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('app_path',''))" 2>/dev/null)
30
+ DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','docker'))" 2>/dev/null)
31
+ ORG_SLUG=$(cat "$CONFIG" | python3 -c "import sys,json; d=json.loads(sys.stdin.read()); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
32
+ APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
33
+ if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
34
+ DATABASE_URL="${DATABASE_URL:-}"
35
+ if [ ! -d "$APP_PATH/node_modules/.prisma" ]; then
36
+ echo "App dependencies not installed. Run: cd $APP_PATH && npm install"
37
+ exit 1
38
+ fi
39
+ SKILL_DIR="${SHOWPANE_TOOLCHAIN_DIR:-$HOME/.showpane/current}"
40
+ SKILL_VERSION=$(cat "$SKILL_DIR/VERSION" 2>/dev/null || echo "unknown")
41
+ echo "SHOWPANE: v$SKILL_VERSION | MODE: $DEPLOY_MODE | APP: $APP_PATH"
42
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
43
+ [ -f "$LEARN_FILE" ] && echo "LEARNINGS: $(wc -l < "$LEARN_FILE" | tr -d ' ') loaded" || echo "LEARNINGS: 0"
44
+
45
+ # Predictive next-skill suggestion
46
+ if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
47
+ _RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
48
+ [ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
49
+ fi
50
+
51
+ # Search relevant learnings
52
+ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
53
+ if [ -f "$LEARN_FILE" ]; then
54
+ _LEARN_COUNT=$(wc -l < "$LEARN_FILE" 2>/dev/null | tr -d ' ')
55
+ echo "LEARNINGS: $_LEARN_COUNT entries"
56
+ if [ "$_LEARN_COUNT" -gt 0 ] 2>/dev/null; then
57
+ echo "RECENT_LEARNINGS:"
58
+ tail -5 "$LEARN_FILE" 2>/dev/null
59
+ fi
60
+ fi
61
+
62
+ # Track skill execution
63
+ SHOWPANE_TIMELINE="$HOME/.showpane/timeline.jsonl"
64
+ mkdir -p "$(dirname "$SHOWPANE_TIMELINE")"
65
+ echo '{"skill":"portal-delete","event":"started","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$SHOWPANE_TIMELINE" 2>/dev/null
66
+ ```
67
+
68
+ If RECENT_SKILLS is shown, suggest the likely next skill:
69
+ - After portal-create → suggest /portal-preview
70
+ - After portal-preview → suggest /portal-deploy or /portal-share
71
+ - After portal-deploy → suggest /portal-status or /portal-verify
72
+ - After portal-setup → suggest /portal-create
73
+ - After portal-credentials → suggest /portal-share
74
+ - After portal-update → suggest /portal-deploy
75
+
76
+ If RECENT_LEARNINGS is shown, review them before proceeding. Past learnings may contain
77
+ relevant warnings or tips for this operation. Apply them where relevant but don't
78
+ mention them unless they directly affect the current task.
79
+
80
+ ## Safety Guard
81
+
82
+ This skill has a PreToolUse guard that warns before destructive operations
83
+ (database resets, file deletion, Vercel project removal). If the guard
84
+ triggers, confirm the action is intentional before proceeding.
85
+
86
+ ## Overview
87
+
88
+ This skill deactivates a client portal by setting `isActive` to `false` in the database. It is a soft delete -- the portal page files remain in the git repository and the database record is preserved, but the portal becomes inaccessible to clients. Existing authenticated sessions and share links stop working because token validation requires an active portal record.
89
+
90
+ Soft delete is intentional. Nothing is truly destroyed. If the user changes their mind, the portal can be reactivated by updating the database record directly. This design avoids the need for a separate "undo" mechanism.
91
+
92
+ ## Steps
93
+
94
+ ### Step 1: Identify the target portal
95
+
96
+ The user must specify which portal to deactivate. If no slug is provided, ask: "Which portal do you want to deactivate? Run /portal list to see your portals."
97
+
98
+ Do not proceed without a confirmed slug.
99
+
100
+ ### Step 2: Confirm with the user
101
+
102
+ Before executing the delete, present a confirmation prompt. This is the one moment where the skill pauses for human input:
103
+
104
+ "This will deactivate portal '<slug>'. Existing sessions will expire within 7 days. The page files will remain in git but won't be accessible. Continue?"
105
+
106
+ Wait for the user to confirm. Accept "yes", "y", "continue", "go ahead", or similar affirmatives. If the user says "no", "cancel", "wait", or anything non-affirmative, abort: "Cancelled. Portal '<slug>' remains active."
107
+
108
+ Do NOT skip this confirmation step. Even though the operation is reversible, deactivating a portal affects live client access. The user should consciously decide.
109
+
110
+ ### Step 3: Execute the deactivation
111
+
112
+ Run the delete script:
113
+
114
+ ```bash
115
+ cd $APP_PATH && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig $APP_PATH/tsconfig.json $SKILL_DIR/bin/delete-portal.ts --slug <slug> --org-id <org_id>
116
+ ```
117
+
118
+ Expected success response:
119
+
120
+ ```json
121
+ {
122
+ "ok": true,
123
+ "slug": "whzan",
124
+ "previousStatus": "active",
125
+ "newStatus": "inactive",
126
+ "sessionsAffected": 3,
127
+ "shareLinksAffected": 1
128
+ }
129
+ ```
130
+
131
+ Expected error responses:
132
+
133
+ - `portal_not_found`: The slug does not exist in this organization. Suggest `/portal list`.
134
+ - `already_inactive`: The portal is already deactivated. Inform the user: "Portal '<slug>' is already inactive. No changes made."
135
+
136
+ ### Step 4: Print confirmation
137
+
138
+ Display a clear confirmation of what happened:
139
+
140
+ ```
141
+ ════════════════════════════════════════
142
+ Portal deactivated: whzan
143
+
144
+ Status: Active -> Inactive
145
+ Sessions affected: 3 (will expire within 7 days)
146
+ Share links affected: 1 (invalidated by deactivation)
147
+
148
+ Page files remain in:
149
+ src/app/(portal)/client/whzan/
150
+ ════════════════════════════════════════
151
+ ```
152
+
153
+ ### Step 5: Explain what happens next
154
+
155
+ After the confirmation box, provide a brief explanation of the consequences:
156
+
157
+ - "The portal URL will return a 'not found' page for any new visitors."
158
+ - "Clients with active sessions can still access the portal until their session expires (up to 7 days)."
159
+ - "The page files (`page.tsx`, `*-client.tsx`) remain in the codebase. You can delete them from git manually if you want to clean up, or leave them for potential reactivation."
160
+
161
+ If the deploy mode is `vercel` or `docker`, note: "The deactivation takes effect immediately -- no redeployment needed. The app checks the `isActive` flag at request time."
162
+
163
+ ### Step 6: Suggest next actions
164
+
165
+ Depending on context:
166
+
167
+ - "To reactivate this portal later, update the database record: set `isActive` to `true` on the ClientPortal with slug '<slug>'."
168
+ - "To permanently remove the page files: `git rm -r src/app/(portal)/client/<slug>/` and commit."
169
+ - "To see your remaining portals: /portal list"
170
+
171
+ ## What Deactivation Does NOT Do
172
+
173
+ Be explicit about what this skill does not touch:
174
+
175
+ 1. **Does not delete database records.** The `ClientPortal` row, credentials, and analytics events are all preserved.
176
+ 2. **Does not delete files.** The React components in `src/app/(portal)/client/<slug>/` remain in the codebase.
177
+ 3. **Does not immediately terminate sessions.** Existing sessions expire on their natural schedule. The `isActive` check happens on new requests, but session tokens may still be valid for their remaining TTL.
178
+ 4. **Does not notify the client.** There is no email or notification sent to the client. If the client needs to know, the user should communicate that separately.
179
+
180
+ ## Edge Cases
181
+
182
+ - **Portal with active share link**: Deactivation invalidates the link because the portal is no longer active. If the portal should stay active but all tokens must be revoked, rotate credentials with `/portal credentials <slug>`.
183
+ - **Portal already inactive**: The script returns `already_inactive`. Do not treat this as an error -- just inform the user.
184
+ - **Last remaining portal**: No special handling. The user can deactivate their last portal. The organization continues to exist with zero active portals.
185
+
186
+ ## Conventions
187
+
188
+ - Always confirm before executing. No exceptions.
189
+ - Use double-line box drawing (`═`) for the confirmation box.
190
+ - Show the file path where page files remain so the user knows where to look if they want to clean up.
191
+ - Do not suggest automatic file deletion. Let the user decide whether to keep or remove the source files.
192
+ - If learnings indicate the user has previously reactivated a portal, emphasize the reversibility more prominently.
193
+
194
+ ## Error Handling
195
+
196
+ - If the preamble fails, stop and display the error.
197
+ - If the portal is not found, suggest `/portal list` to verify the slug.
198
+ - If the database operation fails, show the error from stderr. Common cause: database connection issues.
199
+ - If the user cancels, acknowledge cleanly and stop. Do not ask again.
200
+
201
+ ## Reactivation
202
+
203
+ Deactivation is reversible. To reactivate a portal, you have two options:
204
+
205
+ **Option 1: Direct database update.** Run a query against the database to set `isActive` back to `true`:
206
+
207
+ ```bash
208
+ cd $APP_PATH && npx tsx -e "
209
+ const { PrismaClient } = require('@prisma/client');
210
+ const prisma = new PrismaClient();
211
+ prisma.clientPortal.update({
212
+ where: { slug_organizationId: { slug: '<slug>', organizationId: '<org_id>' } },
213
+ data: { isActive: true }
214
+ }).then(console.log).finally(() => prisma.\$disconnect());
215
+ "
216
+ ```
217
+
218
+ This is intentionally manual. There is no `/portal reactivate` skill in v1. Reactivation should be a deliberate choice, not a casual undo.
219
+
220
+ **Option 2: Re-create the portal.** If the page files were deleted from git after deactivation, the simplest path is to run `/portal create <slug>` again. This creates new page files and a new database record. The old analytics data is tied to the old record and will not carry over.
221
+
222
+ ## Audit Trail
223
+
224
+ The delete operation updates the database record but does not create an explicit audit log entry. However, the state change is visible through:
225
+
226
+ - The `isActive` field on the `ClientPortal` record (false after deactivation)
227
+ - The `updatedAt` timestamp on the record (set to the deactivation time)
228
+ - Git history if the user subsequently removes the page files
229
+
230
+ If learnings are enabled, the skill records the deactivation event:
231
+
232
+ ```json
233
+ {"skill":"portal-delete","key":"deactivated","insight":"whzan deactivated on 2026-04-07","confidence":10,"ts":"2026-04-07T15:00:00Z"}
234
+ ```
235
+
236
+ This helps other skills provide context. For example, `/portal status` can note when a portal was deactivated if the user asks about it.
237
+
238
+ ## Bulk Deactivation
239
+
240
+ If the user wants to deactivate multiple portals at once, handle each one sequentially with a confirmation for each. Do not batch-deactivate without individual confirmation. Each portal represents a client relationship, and the user should consciously decide about each one.
241
+
242
+ If the user explicitly asks to "deactivate all portals" or "shut everything down", confirm once with the full list: "This will deactivate all 5 portals: whzan, acme, example, client-a, client-b. Are you sure?" Then execute them sequentially.
243
+
244
+ ## Deactivation vs Credential Rotation
245
+
246
+ Two operations can revoke client access, and they serve different purposes:
247
+
248
+ - **Deactivation** (this skill): Removes the portal entirely from client view. New visitors see "not found". Existing sessions and share links stop working because the portal is inactive. Use when the client relationship has ended or the portal is no longer needed.
249
+ - **Credential rotation** (`/portal credentials`): Changes the password and invalidates all tokens (sessions + share links). The portal remains active and accessible. Use when credentials may be compromised but the portal should stay live.
250
+
251
+ If the user wants to immediately revoke all access AND deactivate, the sequence is: rotate credentials first (to kill existing sessions immediately), then deactivate (to prevent new logins).
252
+
253
+ ## Completion
254
+
255
+ As a final step, log skill completion:
256
+
257
+ ```bash
258
+ echo '{"skill":"portal-delete","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/timeline.jsonl" 2>/dev/null
259
+ ```
260
+
261
+ ## Related Skills
262
+
263
+ - `/portal list` -- see all portals before deciding which to deactivate
264
+ - `/portal credentials` -- rotate credentials to immediately revoke all access (stronger than deactivate)
265
+ - `/portal status` -- check portal health to identify candidates for deactivation