showpane 0.4.9 → 0.4.11
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.
- package/bundle/meta/scaffold-manifest.json +17 -24
- package/bundle/scaffold/VERSION +1 -1
- package/bundle/scaffold/__dot__env.example +3 -5
- package/bundle/scaffold/package.json +2 -1
- package/bundle/scaffold/prisma/schema.local.prisma +1 -1
- package/bundle/scaffold/prisma/seed.ts +6 -1
- package/bundle/scaffold/prisma.config.ts +5 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +1 -53
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +4 -37
- package/bundle/scaffold/src/__tests__/deploy-bundle.test.ts +48 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +1 -1
- package/bundle/scaffold/src/lib/client-portals.ts +8 -13
- package/bundle/scaffold/src/lib/deploy-bundle.ts +106 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +33 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +2 -32
- package/bundle/scaffold/src/types/adm-zip.d.ts +15 -0
- package/bundle/toolchain/VERSION +1 -1
- package/bundle/toolchain/bin/create-deploy-bundle.ts +3 -72
- package/bundle/toolchain/bin/ensure-cloud-project-link.ts +73 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-create/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +3 -3
- package/bundle/toolchain/skills/portal-delete/SKILL.md +4 -4
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +32 -264
- package/bundle/toolchain/skills/portal-dev/SKILL.md +15 -13
- package/bundle/toolchain/skills/portal-list/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-preview/SKILL.md +9 -23
- package/bundle/toolchain/skills/portal-setup/SKILL.md +19 -28
- package/bundle/toolchain/skills/portal-share/SKILL.md +3 -4
- package/bundle/toolchain/skills/portal-status/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-update/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +2 -2
- package/bundle/toolchain/skills/portal-verify/SKILL.md +21 -33
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +1 -5
- package/bundle/toolchain/skills/shared/preamble.md +2 -2
- package/dist/index.js +56 -10
- package/package.json +1 -1
- package/bundle/scaffold/docker/Caddyfile +0 -3
- package/bundle/scaffold/docker/Dockerfile +0 -30
- package/bundle/scaffold/docker-compose.yml +0 -53
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +0 -143
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +0 -6
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +0 -2
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +0 -3
- package/bundle/scaffold/prisma/schema.prisma +0 -128
- package/bundle/scaffold/scripts/backup.sh +0 -19
- package/bundle/scaffold/scripts/e2e-verify.sh +0 -487
- package/bundle/scaffold/scripts/restore.sh +0 -31
|
@@ -1,54 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
1
|
+
import { createDeployBundle } from "../app/src/lib/deploy-bundle";
|
|
4
2
|
|
|
5
3
|
function fail(message: string): never {
|
|
6
4
|
console.error(JSON.stringify({ ok: false, error: message }));
|
|
7
5
|
process.exit(1);
|
|
8
6
|
}
|
|
9
7
|
|
|
10
|
-
function walkFiles(dir: string, root: string, out: Set<string>) {
|
|
11
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
12
|
-
const fullPath = path.join(dir, entry.name);
|
|
13
|
-
if (entry.isDirectory()) {
|
|
14
|
-
walkFiles(fullPath, root, out);
|
|
15
|
-
continue;
|
|
16
|
-
}
|
|
17
|
-
out.add(path.relative(root, fullPath));
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function collectTracedFiles(appPath: string): Set<string> {
|
|
22
|
-
const files = new Set<string>();
|
|
23
|
-
const outputRoot = path.join(appPath, ".vercel", "output");
|
|
24
|
-
const functionsRoot = path.join(outputRoot, "functions");
|
|
25
|
-
|
|
26
|
-
walkFiles(outputRoot, appPath, files);
|
|
27
|
-
|
|
28
|
-
const queue = [functionsRoot];
|
|
29
|
-
while (queue.length > 0) {
|
|
30
|
-
const current = queue.pop();
|
|
31
|
-
if (!current) continue;
|
|
32
|
-
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
33
|
-
const fullPath = path.join(current, entry.name);
|
|
34
|
-
if (entry.isDirectory()) {
|
|
35
|
-
queue.push(fullPath);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (entry.name !== ".vc-config.json") continue;
|
|
39
|
-
|
|
40
|
-
const config = JSON.parse(readFileSync(fullPath, "utf8")) as {
|
|
41
|
-
filePathMap?: Record<string, string>;
|
|
42
|
-
};
|
|
43
|
-
for (const relativePath of Object.values(config.filePathMap ?? {})) {
|
|
44
|
-
files.add(relativePath);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return files;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
8
|
async function main() {
|
|
53
9
|
const args = process.argv.slice(2);
|
|
54
10
|
const outputIndex = args.indexOf("--output");
|
|
@@ -59,33 +15,8 @@ async function main() {
|
|
|
59
15
|
}
|
|
60
16
|
|
|
61
17
|
const appPath = process.cwd();
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
fail("Missing .vercel/output. Run a prebuilt Vercel build first.");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const zip = new AdmZip();
|
|
68
|
-
const tracedFiles = collectTracedFiles(appPath);
|
|
69
|
-
|
|
70
|
-
for (const relativePath of tracedFiles) {
|
|
71
|
-
const normalized = relativePath.replace(/\\/g, "/");
|
|
72
|
-
|
|
73
|
-
if (normalized === ".env" || normalized.startsWith(".env.")) {
|
|
74
|
-
zip.addFile(normalized, Buffer.from("NODE_ENV=production\n"));
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const fullPath = path.join(appPath, relativePath);
|
|
79
|
-
const stat = statSync(fullPath, { throwIfNoEntry: false });
|
|
80
|
-
if (!stat?.isFile()) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
zip.addLocalFile(fullPath, path.dirname(normalized), path.basename(normalized));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
zip.writeZip(outputPath);
|
|
88
|
-
console.log(JSON.stringify({ ok: true, outputPath, fileCount: tracedFiles.size }));
|
|
18
|
+
const { fileCount } = createDeployBundle(appPath, outputPath);
|
|
19
|
+
console.log(JSON.stringify({ ok: true, outputPath, fileCount }));
|
|
89
20
|
}
|
|
90
21
|
|
|
91
22
|
main().catch((error) => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
type ShowpaneConfig = {
|
|
5
|
+
accessToken?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type CloudProjectLink = {
|
|
9
|
+
projectId: string;
|
|
10
|
+
orgId: string;
|
|
11
|
+
projectName: string;
|
|
12
|
+
settings: {
|
|
13
|
+
createdAt: number;
|
|
14
|
+
framework: string;
|
|
15
|
+
devCommand: string | null;
|
|
16
|
+
installCommand: string | null;
|
|
17
|
+
buildCommand: string | null;
|
|
18
|
+
outputDirectory: string | null;
|
|
19
|
+
rootDirectory: string | null;
|
|
20
|
+
directoryListing: boolean;
|
|
21
|
+
nodeVersion: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function fail(message: string): never {
|
|
26
|
+
console.error(JSON.stringify({ ok: false, error: message }));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConfig(): ShowpaneConfig {
|
|
31
|
+
const configPath = path.join(process.env.HOME || "", ".showpane", "config.json");
|
|
32
|
+
if (!fs.existsSync(configPath)) {
|
|
33
|
+
fail("Showpane not configured");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return JSON.parse(fs.readFileSync(configPath, "utf8")) as ShowpaneConfig;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function main() {
|
|
40
|
+
const config = getConfig();
|
|
41
|
+
if (!config.accessToken) {
|
|
42
|
+
fail("Missing cloud access token. Run showpane login.");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const apiBase = process.env.SHOWPANE_CLOUD_URL || "https://app.showpane.com";
|
|
46
|
+
const res = await fetch(`${apiBase}/api/cli/project-link`, {
|
|
47
|
+
headers: {
|
|
48
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
fail(`Could not fetch cloud project link (${res.status})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const projectLink = await res.json() as CloudProjectLink;
|
|
57
|
+
const vercelDir = path.join(process.cwd(), ".vercel");
|
|
58
|
+
fs.mkdirSync(vercelDir, { recursive: true });
|
|
59
|
+
fs.writeFileSync(
|
|
60
|
+
path.join(vercelDir, "project.json"),
|
|
61
|
+
`${JSON.stringify(projectLink, null, 2)}\n`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
console.log(JSON.stringify({
|
|
65
|
+
ok: true,
|
|
66
|
+
path: path.join(vercelDir, "project.json"),
|
|
67
|
+
projectId: projectLink.projectId,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
main().catch((error) => {
|
|
72
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
73
|
+
});
|
|
@@ -17,7 +17,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
17
17
|
exit 1
|
|
18
18
|
fi
|
|
19
19
|
APP_PATH=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('app_path',''))" 2>/dev/null)
|
|
20
|
-
DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','
|
|
20
|
+
DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','local'))" 2>/dev/null)
|
|
21
21
|
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)
|
|
22
22
|
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
23
23
|
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
@@ -34,7 +34,7 @@ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
|
34
34
|
|
|
35
35
|
# Predictive next-skill suggestion
|
|
36
36
|
if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
|
|
37
|
-
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
|
|
37
|
+
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//' || true)
|
|
38
38
|
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
39
39
|
fi
|
|
40
40
|
|
|
@@ -19,7 +19,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
19
19
|
exit 1
|
|
20
20
|
fi
|
|
21
21
|
APP_PATH=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
|
|
22
|
-
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','
|
|
22
|
+
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null)
|
|
23
23
|
ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
|
|
24
24
|
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
25
25
|
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
@@ -36,7 +36,7 @@ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
|
36
36
|
|
|
37
37
|
# Predictive next-skill suggestion
|
|
38
38
|
if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
|
|
39
|
-
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
|
|
39
|
+
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//' || true)
|
|
40
40
|
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
41
41
|
fi
|
|
42
42
|
|
|
@@ -26,7 +26,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
26
26
|
exit 1
|
|
27
27
|
fi
|
|
28
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','
|
|
29
|
+
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null)
|
|
30
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
31
|
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
32
32
|
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
@@ -43,7 +43,7 @@ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
|
43
43
|
|
|
44
44
|
# Predictive next-skill suggestion
|
|
45
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/,$//')
|
|
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/,$//' || true)
|
|
47
47
|
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
48
48
|
fi
|
|
49
49
|
|
|
@@ -128,7 +128,7 @@ The script returns JSON on stdout:
|
|
|
128
128
|
If the script returns an error, handle it based on the error type:
|
|
129
129
|
|
|
130
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
|
|
131
|
+
- `database_error` — Connection or query failure. Check DATABASE_URL and re-apply the local schema with `cd $APP_PATH && npm run prisma:db-push`.
|
|
132
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
133
|
|
|
134
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.
|
|
@@ -27,7 +27,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
27
27
|
exit 1
|
|
28
28
|
fi
|
|
29
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','
|
|
30
|
+
DEPLOY_MODE=$(cat "$CONFIG" | python3 -c "import sys,json; print(json.loads(sys.stdin.read()).get('deploy_mode','local'))" 2>/dev/null)
|
|
31
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
32
|
APP_PATH="${SHOWPANE_APP_PATH:-$APP_PATH}"
|
|
33
33
|
if [ -f "$APP_PATH/.env" ]; then set -a && source "$APP_PATH/.env" && set +a; fi
|
|
@@ -44,7 +44,7 @@ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
|
44
44
|
|
|
45
45
|
# Predictive next-skill suggestion
|
|
46
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/,$//')
|
|
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/,$//' || true)
|
|
48
48
|
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
49
49
|
fi
|
|
50
50
|
|
|
@@ -80,7 +80,7 @@ mention them unless they directly affect the current task.
|
|
|
80
80
|
## Safety Guard
|
|
81
81
|
|
|
82
82
|
This skill has a PreToolUse guard that warns before destructive operations
|
|
83
|
-
(database resets
|
|
83
|
+
(database resets and file deletion). If the guard
|
|
84
84
|
triggers, confirm the action is intentional before proceeding.
|
|
85
85
|
|
|
86
86
|
## Overview
|
|
@@ -158,7 +158,7 @@ After the confirmation box, provide a brief explanation of the consequences:
|
|
|
158
158
|
- "Clients with active sessions can still access the portal until their session expires (up to 7 days)."
|
|
159
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
160
|
|
|
161
|
-
|
|
161
|
+
Note: "The deactivation takes effect immediately -- no redeployment needed. The app checks the `isActive` flag at request time."
|
|
162
162
|
|
|
163
163
|
### Step 6: Suggest next actions
|
|
164
164
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: portal-deploy
|
|
3
3
|
description: |
|
|
4
|
-
|
|
4
|
+
Publish the Showpane portal app to Showpane Cloud. Runs pre-flight checks, builds the app locally, uploads the artifact, and waits for the hosted publish to go live.
|
|
5
5
|
Trigger phrases: "portal deploy", "deploy portals", "push to production", "ship the portals". (showpane)
|
|
6
6
|
allowed-tools: [Bash, Read, Write, Edit, Glob, Grep, WebSearch]
|
|
7
7
|
---
|
|
@@ -19,7 +19,7 @@ if [ ! -f "$CONFIG" ]; then
|
|
|
19
19
|
exit 1
|
|
20
20
|
fi
|
|
21
21
|
APP_PATH=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('app_path',''))" 2>/dev/null)
|
|
22
|
-
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','
|
|
22
|
+
DEPLOY_MODE=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('deploy_mode','local'))" 2>/dev/null)
|
|
23
23
|
ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('orgSlug','') or d.get('org_slug',''))" 2>/dev/null)
|
|
24
24
|
CLOUD_API_TOKEN=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('cloud',d).get('api_token', d.get('accessToken','')))" 2>/dev/null)
|
|
25
25
|
CLOUD_ORG_SLUG=$(python3 -c "import json; d=json.load(open('$CONFIG')); print(d.get('cloud',d).get('org_slug', d.get('orgSlug','')))" 2>/dev/null)
|
|
@@ -40,7 +40,7 @@ LEARN_FILE="$HOME/.showpane/learnings.jsonl"
|
|
|
40
40
|
|
|
41
41
|
# Predictive next-skill suggestion
|
|
42
42
|
if [ -f "$HOME/.showpane/timeline.jsonl" ]; then
|
|
43
|
-
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//')
|
|
43
|
+
_RECENT=$(grep '"event":"completed"' "$HOME/.showpane/timeline.jsonl" 2>/dev/null | tail -3 | grep -o '"skill":"[^"]*"' | sed 's/"skill":"//;s/"//' | tr '\n' ',' | sed 's/,$//' || true)
|
|
44
44
|
[ -n "$_RECENT" ] && echo "RECENT_SKILLS: $_RECENT"
|
|
45
45
|
fi
|
|
46
46
|
|
|
@@ -75,237 +75,12 @@ mention them unless they directly affect the current task.
|
|
|
75
75
|
|
|
76
76
|
## Steps
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
This skill always publishes to Showpane Cloud.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
- If `CLOUD_API_TOKEN` is missing, stop and tell the user to run `showpane login`.
|
|
81
|
+
- If `DEPLOY_MODE` is `local`, that is fine — local is the normal authoring mode. The deploy target is still Showpane Cloud.
|
|
81
82
|
|
|
82
|
-
|
|
83
|
-
- If `DEPLOY_MODE` is `"docker"` or `"vercel"` or unset — continue with the existing self-hosted deploy flow (Steps 1-6)
|
|
84
|
-
|
|
85
|
-
If no deploy mode is configured, present the options:
|
|
86
|
-
|
|
87
|
-
1. **Self-host with Docker** (free) — Deploy with Docker Compose on your own server
|
|
88
|
-
2. **Showpane Cloud** ($29/month, 7-day free trial) — Managed hosting at {org}.showpane.com
|
|
89
|
-
|
|
90
|
-
If the user chooses Cloud but has not run `showpane login` yet (no `CLOUD_API_TOKEN`), tell them:
|
|
91
|
-
|
|
92
|
-
> "Run `showpane login` first to authenticate with Showpane Cloud, then re-run this deploy."
|
|
93
|
-
|
|
94
|
-
### Step 1: Pre-flight checks
|
|
95
|
-
|
|
96
|
-
Run all checks before deploying. Any failure here should block the deploy.
|
|
97
|
-
|
|
98
|
-
#### 1a. TypeScript type check
|
|
99
|
-
|
|
100
|
-
```bash
|
|
101
|
-
cd "$APP_PATH" && npx tsc --noEmit 2>&1
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
If type errors are found, display them clearly and stop. Do not deploy with type errors. Common type errors during portal development:
|
|
105
|
-
|
|
106
|
-
- Missing imports (forgot to import an icon from lucide-react)
|
|
107
|
-
- PortalShell prop mismatch (wrong type for contact, missing required prop)
|
|
108
|
-
- Unused variables in tab content functions
|
|
109
|
-
|
|
110
|
-
For simple issues (missing import, typo in a prop name), offer to fix them before retrying the deploy. For complex type errors in shared code, suggest the user investigate and fix manually.
|
|
111
|
-
|
|
112
|
-
The type check typically takes 10-30 seconds depending on project size. If it takes longer than 60 seconds, it may indicate a problem with the TypeScript configuration.
|
|
113
|
-
|
|
114
|
-
#### 1b. Verify all portals have credentials
|
|
115
|
-
|
|
116
|
-
Run the list-portals script to check portal status:
|
|
117
|
-
|
|
118
|
-
```bash
|
|
119
|
-
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>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
The output is a JSON array of portals. Check each portal for a `username` field. If any active portal lacks credentials, warn the user:
|
|
123
|
-
|
|
124
|
-
```
|
|
125
|
-
WARNING: The following portals have no login credentials:
|
|
126
|
-
- acme-health
|
|
127
|
-
- new-client
|
|
128
|
-
|
|
129
|
-
Clients won't be able to log in to these portals.
|
|
130
|
-
Run /portal credentials <slug> to create credentials, or continue anyway?
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
This is a warning, not a blocker — the user can choose to continue. Some portals may be works in progress that don't need credentials yet.
|
|
134
|
-
|
|
135
|
-
#### 1c. Verify deployment config exists
|
|
136
|
-
|
|
137
|
-
**For Docker mode:**
|
|
138
|
-
```bash
|
|
139
|
-
ls "$APP_PATH/docker-compose.yml" 2>/dev/null || ls "$APP_PATH/compose.yml" 2>/dev/null
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
**For Vercel mode:**
|
|
143
|
-
```bash
|
|
144
|
-
ls "$APP_PATH/.vercel/project.json" 2>/dev/null || ls "$APP_PATH/vercel.json" 2>/dev/null
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
If the config doesn't exist, inform the user and provide setup guidance:
|
|
148
|
-
- Docker: "No docker-compose.yml found. Create one or switch deploy mode to vercel."
|
|
149
|
-
- Vercel: "No Vercel config found. Run `npx vercel link` to connect your project."
|
|
150
|
-
|
|
151
|
-
#### 1d. Check for uncommitted changes (Vercel mode only)
|
|
152
|
-
|
|
153
|
-
For Vercel deploys, check if there are uncommitted changes:
|
|
154
|
-
|
|
155
|
-
```bash
|
|
156
|
-
cd "$APP_PATH" && git status --porcelain
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
If there are changes, show the user what will be committed as part of the deploy:
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
cd "$APP_PATH" && git diff --stat
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
List the changed files and ask the user to confirm. Pay attention to:
|
|
166
|
-
- `.env` files — these should NEVER be committed. If `.env` appears in the changes, warn the user and exclude it.
|
|
167
|
-
- Large binary files — these will slow down the deploy. Suggest adding them to `.gitignore`.
|
|
168
|
-
- Files outside the portal directories — these may be unintended changes.
|
|
169
|
-
|
|
170
|
-
If there are no changes to commit and the current branch is up to date with the remote, inform the user that there is nothing new to deploy.
|
|
171
|
-
|
|
172
|
-
### Step 2: Apply database migrations
|
|
173
|
-
|
|
174
|
-
Before deploying, apply any pending migrations:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
cd "$APP_PATH" && npx prisma migrate deploy
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
This runs in production mode — it only applies pending migrations, never creates new ones. If this fails, stop the deploy and show the error. Common issues:
|
|
181
|
-
- DATABASE_URL not set or incorrect
|
|
182
|
-
- Database server unreachable
|
|
183
|
-
- Migration conflicts (rare, requires manual resolution)
|
|
184
|
-
|
|
185
|
-
### Step 3: Deploy
|
|
186
|
-
|
|
187
|
-
#### Docker mode
|
|
188
|
-
|
|
189
|
-
Build and restart the containers:
|
|
190
|
-
|
|
191
|
-
```bash
|
|
192
|
-
cd "$APP_PATH" && docker compose build && docker compose up -d
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
If `docker compose` is not available, try `docker-compose` (older syntax):
|
|
196
|
-
|
|
197
|
-
```bash
|
|
198
|
-
cd "$APP_PATH" && docker-compose build && docker-compose up -d
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
Wait for the containers to start. Check container status:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
cd "$APP_PATH" && docker compose ps
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
All containers should show status "Up" or "running".
|
|
208
|
-
|
|
209
|
-
#### Vercel mode
|
|
210
|
-
|
|
211
|
-
Stage, commit, and push:
|
|
212
|
-
|
|
213
|
-
```bash
|
|
214
|
-
cd "$APP_PATH" && git add -A && git commit -m "Deploy portal updates" && git push
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
If there are no changes to commit, just push:
|
|
218
|
-
|
|
219
|
-
```bash
|
|
220
|
-
cd "$APP_PATH" && git push
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
The push triggers Vercel's automatic deployment pipeline. Note that the deploy is async — the push returns immediately, but the actual deployment takes 1-3 minutes.
|
|
224
|
-
|
|
225
|
-
For Vercel, inform the user:
|
|
226
|
-
|
|
227
|
-
> "Pushed to remote. Vercel will build and deploy automatically. Check your Vercel dashboard for deploy status."
|
|
228
|
-
|
|
229
|
-
### Step 4: Post-deploy verification
|
|
230
|
-
|
|
231
|
-
#### Docker mode
|
|
232
|
-
|
|
233
|
-
Wait a few seconds for the app to start, then hit the health endpoint:
|
|
234
|
-
|
|
235
|
-
```bash
|
|
236
|
-
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/health
|
|
237
|
-
```
|
|
238
|
-
|
|
239
|
-
Expected: HTTP 200. Then check the response body:
|
|
240
|
-
|
|
241
|
-
```bash
|
|
242
|
-
curl -s http://localhost:8080/api/health
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Expected: `{"status":"ok"}` or similar health response.
|
|
246
|
-
|
|
247
|
-
If the health check fails:
|
|
248
|
-
1. Check container logs: `cd "$APP_PATH" && docker compose logs --tail=50`
|
|
249
|
-
2. Check if the port is correct (might be 3000, 8080, or custom)
|
|
250
|
-
3. Report the error and suggest debugging steps
|
|
251
|
-
|
|
252
|
-
#### Vercel mode
|
|
253
|
-
|
|
254
|
-
If a production URL is known (from config or Vercel project settings), check it:
|
|
255
|
-
|
|
256
|
-
```bash
|
|
257
|
-
curl -s -o /dev/null -w "%{http_code}" https://<production_url>/api/health
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
If the production URL is not known, skip the health check and inform the user:
|
|
261
|
-
|
|
262
|
-
> "Deploy triggered. Verify at your Vercel production URL once the build completes."
|
|
263
|
-
|
|
264
|
-
### Step 5: Deployment summary
|
|
265
|
-
|
|
266
|
-
Print a clear summary of what happened:
|
|
267
|
-
|
|
268
|
-
```
|
|
269
|
-
Deploy complete!
|
|
270
|
-
|
|
271
|
-
Mode: docker
|
|
272
|
-
Migrations: 2 applied (or: up to date)
|
|
273
|
-
Type check: passed
|
|
274
|
-
Portals: 5 active (3 with credentials)
|
|
275
|
-
Health: OK (200)
|
|
276
|
-
|
|
277
|
-
App URL: http://localhost:8080
|
|
278
|
-
Login: http://localhost:8080/client
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
For Vercel:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
Deploy triggered!
|
|
285
|
-
|
|
286
|
-
Mode: vercel
|
|
287
|
-
Migrations: up to date
|
|
288
|
-
Type check: passed
|
|
289
|
-
Commit: abc1234 "Deploy portal updates"
|
|
290
|
-
Portals: 5 active (3 with credentials)
|
|
291
|
-
|
|
292
|
-
Status: Building (check Vercel dashboard)
|
|
293
|
-
URL: https://your-app.vercel.app
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Step 6: Record deployment
|
|
297
|
-
|
|
298
|
-
Log the deployment for operational memory:
|
|
299
|
-
|
|
300
|
-
```bash
|
|
301
|
-
echo '{"skill":"portal-deploy","key":"deploy","insight":"Deployed via <mode>. Migrations: <count>. Portals: <count> active.","confidence":10,"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> "$HOME/.showpane/learnings.jsonl"
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
---
|
|
305
|
-
|
|
306
|
-
## Cloud Deploy Flow (when DEPLOY_MODE is "cloud")
|
|
307
|
-
|
|
308
|
-
This section runs instead of Steps 1-6 when `DEPLOY_MODE` is `"cloud"`. The cloud flow builds the app locally, packages the `.vercel/output` artifact, uploads that artifact to Showpane Cloud, and lets the control plane publish the hosted app. The OSS app should never need to reason about Vercel project IDs or call the Vercel API directly.
|
|
83
|
+
The flow below builds the app locally, packages the `.vercel/output` artifact, uploads that artifact to Showpane Cloud, and lets the control plane publish the hosted app. The OSS app should never need to reason about provider project IDs or call provider APIs directly.
|
|
309
84
|
|
|
310
85
|
### Cloud Step 1: Pre-flight checks
|
|
311
86
|
|
|
@@ -328,11 +103,11 @@ If the token is missing, stop and tell the user to run `showpane login`.
|
|
|
328
103
|
curl -s -o /dev/null -w "%{http_code}" "$CLOUD_API_BASE/api/health"
|
|
329
104
|
```
|
|
330
105
|
|
|
331
|
-
Expected: HTTP 200. If Showpane Cloud is unreachable, stop and show the error. Token validity is checked by the deploy API itself — OSS should not call
|
|
106
|
+
Expected: HTTP 200. If Showpane Cloud is unreachable, stop and show the error. Token validity is checked by the deploy API itself — OSS should not call provider internals directly.
|
|
332
107
|
|
|
333
108
|
#### 1c. TypeScript type check
|
|
334
109
|
|
|
335
|
-
|
|
110
|
+
Run the standard type check:
|
|
336
111
|
|
|
337
112
|
```bash
|
|
338
113
|
cd "$APP_PATH" && npx tsc --noEmit 2>&1
|
|
@@ -342,14 +117,22 @@ If type errors are found, display them and stop. Offer to fix simple issues (mis
|
|
|
342
117
|
|
|
343
118
|
#### 1d. Verify portals have credentials
|
|
344
119
|
|
|
345
|
-
|
|
120
|
+
Run list-portals and warn about portals missing credentials. This is a warning, not a blocker.
|
|
346
121
|
|
|
347
122
|
### Cloud Step 2: Build the app
|
|
348
123
|
|
|
349
|
-
|
|
124
|
+
Before building, ensure the hidden local project-link metadata exists:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
if [ ! -f "$APP_PATH/.vercel/project.json" ]; then
|
|
128
|
+
cd "$APP_PATH" && NODE_PATH="$APP_PATH/node_modules" npx tsx --tsconfig "$APP_PATH/tsconfig.json" "$SKILL_DIR/bin/ensure-cloud-project-link.ts"
|
|
129
|
+
fi
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Run the cloud build command that produces the prebuilt artifact:
|
|
350
133
|
|
|
351
134
|
```bash
|
|
352
|
-
cd "$APP_PATH" &&
|
|
135
|
+
cd "$APP_PATH" && npm run cloud:build
|
|
353
136
|
```
|
|
354
137
|
|
|
355
138
|
After the build completes, verify the output directory was created:
|
|
@@ -591,12 +374,12 @@ rm -f "$FILE_MANIFEST_PATH"
|
|
|
591
374
|
|
|
592
375
|
## Post-Deploy Verification
|
|
593
376
|
|
|
594
|
-
After deployment succeeds
|
|
377
|
+
After deployment succeeds, automatically run these verification steps. Do not ask the user — just do them.
|
|
595
378
|
|
|
596
379
|
### Step V1: DNS/URL Check
|
|
597
380
|
Fetch the portal URL and verify it returns 200:
|
|
598
381
|
```bash
|
|
599
|
-
PORTAL_URL="${CLOUD_PORTAL_URL:-
|
|
382
|
+
PORTAL_URL="${CLOUD_PORTAL_URL:-https://$CLOUD_ORG_SLUG.showpane.com}"
|
|
600
383
|
echo "Verifying $PORTAL_URL..."
|
|
601
384
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$PORTAL_URL" 2>/dev/null)
|
|
602
385
|
echo "HTTP status: $HTTP_CODE"
|
|
@@ -646,24 +429,17 @@ Next: /portal-status for ongoing monitoring
|
|
|
646
429
|
|
|
647
430
|
Deployments can fail at multiple points. Here is how to recover from each:
|
|
648
431
|
|
|
649
|
-
###
|
|
650
|
-
If
|
|
651
|
-
- Check the
|
|
652
|
-
- Check that
|
|
653
|
-
- Check available disk space
|
|
654
|
-
- Try `docker compose build --no-cache` if a cached layer is stale
|
|
655
|
-
|
|
656
|
-
### Vercel push failure
|
|
657
|
-
If `git push` fails:
|
|
658
|
-
- **Authentication**: Check git credentials or SSH keys
|
|
659
|
-
- **Remote rejection**: The remote may have branch protections. Check if pushing to the correct branch.
|
|
660
|
-
- **Diverged history**: Someone else pushed since your last pull. Run `git pull --rebase` first.
|
|
432
|
+
### Local build failure
|
|
433
|
+
If the local build fails:
|
|
434
|
+
- Check the type and build output directly
|
|
435
|
+
- Check that `.env` contains the expected local SQLite settings
|
|
436
|
+
- Check available disk space for the Next.js build and artifact zip
|
|
661
437
|
|
|
662
438
|
### Health check failure
|
|
663
439
|
If the health endpoint returns non-200 after deploy:
|
|
664
|
-
- Check
|
|
665
|
-
- Common causes:
|
|
666
|
-
-
|
|
440
|
+
- Check the deployment status from `GET /api/deployments/$DEPLOY_ID`
|
|
441
|
+
- Common causes: DNS propagation, hosted runtime config issues, or a bad artifact upload
|
|
442
|
+
- Report the exact hosted error if Showpane Cloud returns one
|
|
667
443
|
|
|
668
444
|
### Cloud: Token invalid or auth expired
|
|
669
445
|
If the Showpane Cloud API returns 401/403 during pre-flight or publish:
|
|
@@ -688,7 +464,7 @@ If `POST /api/files/plan` or `POST /api/files/upload` fails:
|
|
|
688
464
|
If the deployment never reaches `live`:
|
|
689
465
|
- Check the response from `GET /api/deployments/$DEPLOY_ID` for an `error` field.
|
|
690
466
|
- Report the failure exactly as returned by Showpane Cloud.
|
|
691
|
-
- Do not tell the user to debug
|
|
467
|
+
- Do not tell the user to debug provider internals directly unless the cloud response explicitly points there.
|
|
692
468
|
|
|
693
469
|
## Completion
|
|
694
470
|
|
|
@@ -702,20 +478,12 @@ echo '{"skill":"portal-deploy","event":"completed","ts":"'$(date -u +%Y-%m-%dT%H
|
|
|
702
478
|
|
|
703
479
|
- Always run pre-flight checks before deploying — never skip the type check
|
|
704
480
|
- Credential warnings are non-blocking — the user decides whether to continue
|
|
705
|
-
- Migrations run before the deploy, not after
|
|
706
|
-
- For Docker: verify the health endpoint after deploy
|
|
707
|
-
- For Vercel: the deploy is async — inform the user to check the dashboard
|
|
708
|
-
- Never force-push or reset git history during Vercel deploys
|
|
709
481
|
- If any pre-flight check fails (type errors, missing deploy config), stop and explain
|
|
710
482
|
- Show the full deployment summary with portal count, migration status, and health
|
|
711
|
-
- The deploy commit message is always "Deploy portal updates" — keep it simple and consistent
|
|
712
|
-
- If the user wants a custom commit message, they should commit manually before running deploy
|
|
713
|
-
- For Vercel deploys, the build typically takes 1-3 minutes — do not poll or wait, just inform the user
|
|
714
|
-
- Always run migrations before the build/push step, never after — the app code expects the latest schema
|
|
715
483
|
- If this is the first deploy, suggest running `/portal credentials` for all portals before deploying so clients can actually log in
|
|
716
484
|
- For Cloud deploys: build locally, upload the artifact to Showpane Cloud, and let the control plane publish it
|
|
717
485
|
- For Cloud deploys: always wait for the deployment to reach `live` before declaring success
|
|
718
486
|
- For Cloud deploys: the portal URL is `https://{org}.showpane.com` — verify it returns 200 after deploy
|
|
719
487
|
- For Cloud deploys: clean up the temporary artifact after deploy completes
|
|
720
488
|
- The CLI `showpane login` is auth only — org creation and billing live in Showpane Cloud checkout
|
|
721
|
-
- For Cloud deploys: OSS should never call
|
|
489
|
+
- For Cloud deploys: OSS should never call provider APIs directly or require provider details from the user
|