showpane 0.4.1 → 0.4.3
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/README.md +26 -1
- package/bundle/meta/scaffold-manifest.json +73 -0
- package/bundle/scaffold/VERSION +1 -0
- package/bundle/scaffold/__dot__env.example +24 -0
- package/bundle/scaffold/__dot__gitignore +41 -0
- package/bundle/scaffold/docker/Caddyfile +3 -0
- package/bundle/scaffold/docker/Dockerfile +30 -0
- package/bundle/scaffold/docker-compose.yml +53 -0
- package/bundle/scaffold/next.config.ts +20 -0
- package/bundle/scaffold/package-lock.json +5843 -0
- package/bundle/scaffold/package.json +42 -0
- package/bundle/scaffold/postcss.config.js +6 -0
- package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
- package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
- package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
- package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
- package/bundle/scaffold/prisma/schema.local.prisma +131 -0
- package/bundle/scaffold/prisma/schema.prisma +128 -0
- package/bundle/scaffold/prisma/seed.ts +49 -0
- package/bundle/scaffold/public/example-avatar.svg +4 -0
- package/bundle/scaffold/public/example-logo.svg +4 -0
- package/bundle/scaffold/public/robots.txt +2 -0
- package/bundle/scaffold/scripts/backup.sh +19 -0
- package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
- package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
- package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
- package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
- package/bundle/scaffold/scripts/restore.sh +31 -0
- package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
- package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
- package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
- package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
- package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
- package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
- package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
- package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
- package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
- package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
- package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
- package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
- package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
- package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
- package/bundle/scaffold/src/app/api/health/route.ts +19 -0
- package/bundle/scaffold/src/app/globals.css +7 -0
- package/bundle/scaffold/src/app/layout.tsx +25 -0
- package/bundle/scaffold/src/app/page.tsx +177 -0
- package/bundle/scaffold/src/components/portal-login.tsx +169 -0
- package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
- package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
- package/bundle/scaffold/src/lib/branding.ts +50 -0
- package/bundle/scaffold/src/lib/client-auth.ts +98 -0
- package/bundle/scaffold/src/lib/client-portals.ts +134 -0
- package/bundle/scaffold/src/lib/control-plane.ts +100 -0
- package/bundle/scaffold/src/lib/db.ts +7 -0
- package/bundle/scaffold/src/lib/files.ts +124 -0
- package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
- package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
- package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
- package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
- package/bundle/scaffold/src/lib/storage.ts +204 -0
- package/bundle/scaffold/src/lib/token.ts +186 -0
- package/bundle/scaffold/src/lib/utils.ts +6 -0
- package/bundle/scaffold/src/middleware.ts +61 -0
- package/bundle/scaffold/tailwind.config.ts +15 -0
- package/bundle/scaffold/tests/__dot__gitkeep +0 -0
- package/bundle/scaffold/tsconfig.json +23 -0
- package/bundle/scaffold/vitest.config.ts +13 -0
- package/bundle/toolchain/VERSION +1 -0
- package/bundle/toolchain/bin/check-slug.ts +59 -0
- package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
- package/bundle/toolchain/bin/create-portal.ts +71 -0
- package/bundle/toolchain/bin/delete-portal.ts +48 -0
- package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
- package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
- package/bundle/toolchain/bin/generate-share-link.ts +68 -0
- package/bundle/toolchain/bin/list-portals.ts +53 -0
- package/bundle/toolchain/bin/materialize-file.ts +35 -0
- package/bundle/toolchain/bin/query-analytics.ts +88 -0
- package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
- package/bundle/toolchain/bin/showpane-config +63 -0
- package/bundle/toolchain/bin/tsconfig.json +13 -0
- package/bundle/toolchain/skills/VERSION +1 -0
- package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
- package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
- package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
- package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
- package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
- package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
- package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
- package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
- package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
- package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
- package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
- package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
- package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
- package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
- package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
- package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
- package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
- package/bundle/toolchain/skills/shared/preamble.md +137 -0
- package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
- package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
- package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
- package/dist/index.js +1248 -164
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
+
import { execSync, spawn } from "child_process";
|
|
5
|
+
import { randomBytes, createHash } from "crypto";
|
|
4
6
|
import { createInterface } from "readline";
|
|
5
|
-
import { execSync, spawn, exec } from "child_process";
|
|
6
|
-
import fs from "fs";
|
|
7
|
-
import { randomBytes } from "crypto";
|
|
8
7
|
import { createServer } from "net";
|
|
9
|
-
import
|
|
8
|
+
import fs from "fs";
|
|
10
9
|
import os from "os";
|
|
10
|
+
import path from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
|
-
var {
|
|
13
|
-
|
|
12
|
+
var {
|
|
13
|
+
chmodSync,
|
|
14
|
+
cpSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
lstatSync,
|
|
17
|
+
mkdirSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readlinkSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
symlinkSync,
|
|
23
|
+
unlinkSync,
|
|
24
|
+
writeFileSync
|
|
25
|
+
} = fs;
|
|
26
|
+
var { basename, dirname, join, resolve } = path;
|
|
14
27
|
var { homedir } = os;
|
|
15
28
|
var RESET = "\x1B[0m";
|
|
16
29
|
var BOLD = "\x1B[1m";
|
|
@@ -19,14 +32,38 @@ var GREEN = "\x1B[32m";
|
|
|
19
32
|
var BLUE = "\x1B[34m";
|
|
20
33
|
var WHITE = "\x1B[37m";
|
|
21
34
|
var RED = "\x1B[31m";
|
|
22
|
-
|
|
23
|
-
|
|
35
|
+
var API_BASE = "https://app.showpane.com";
|
|
36
|
+
var SHOWPANE_HOME = join(homedir(), ".showpane");
|
|
37
|
+
var SHOWPANE_BIN_DIR = join(SHOWPANE_HOME, "bin");
|
|
38
|
+
var TOOLCHAIN_DIR = join(SHOWPANE_HOME, "toolchains");
|
|
39
|
+
var CURRENT_TOOLCHAIN_LINK = join(SHOWPANE_HOME, "current");
|
|
40
|
+
var CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
41
|
+
var SHOWPANE_SHARED_SKILL = "showpane-shared";
|
|
42
|
+
var METADATA_DIRNAME = ".showpane";
|
|
43
|
+
var PROJECT_METADATA_FILE = "project.json";
|
|
44
|
+
var MANAGED_FILES_FILE = "managed-files.json";
|
|
45
|
+
var StepCommandError = class extends Error {
|
|
46
|
+
output;
|
|
47
|
+
constructor(message, output = "") {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "StepCommandError";
|
|
50
|
+
this.output = output;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
function green(message) {
|
|
54
|
+
console.log(` ${GREEN}\u2713${RESET} ${message}`);
|
|
55
|
+
}
|
|
56
|
+
function blue(message) {
|
|
57
|
+
console.log(` ${BLUE}\u2192${RESET} ${message}`);
|
|
58
|
+
}
|
|
59
|
+
function error(message) {
|
|
60
|
+
console.error(` ${RED}\u2717${RESET} ${message}`);
|
|
24
61
|
}
|
|
25
|
-
function
|
|
26
|
-
console.log(
|
|
62
|
+
function printCreateUsage() {
|
|
63
|
+
console.log("Usage: showpane [--yes --name <company>] [--no-open] [--verbose]");
|
|
27
64
|
}
|
|
28
|
-
function
|
|
29
|
-
console.
|
|
65
|
+
function printClaudeUsage() {
|
|
66
|
+
console.log("Usage: showpane claude [--project <name-or-path>] [--yes --name <company>] [--verbose]");
|
|
30
67
|
}
|
|
31
68
|
function printBanner() {
|
|
32
69
|
const banner = `
|
|
@@ -36,7 +73,7 @@ ${BOLD}${WHITE} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2
|
|
|
36
73
|
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D
|
|
37
74
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2554\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
38
75
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u255D\u255A\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D${RESET}
|
|
39
|
-
${DIM}
|
|
76
|
+
${DIM} AI-powered client portals.${RESET}
|
|
40
77
|
`;
|
|
41
78
|
console.log(banner);
|
|
42
79
|
}
|
|
@@ -45,185 +82,1208 @@ function ask(question) {
|
|
|
45
82
|
input: process.stdin,
|
|
46
83
|
output: process.stdout
|
|
47
84
|
});
|
|
48
|
-
return new Promise((
|
|
85
|
+
return new Promise((resolveAnswer) => {
|
|
49
86
|
rl.question(question, (answer) => {
|
|
50
87
|
rl.close();
|
|
51
|
-
|
|
88
|
+
resolveAnswer(answer.trim());
|
|
52
89
|
});
|
|
53
90
|
});
|
|
54
91
|
}
|
|
92
|
+
function parseCreateArgs(args) {
|
|
93
|
+
const options = {
|
|
94
|
+
noOpen: false,
|
|
95
|
+
verbose: false,
|
|
96
|
+
yes: false
|
|
97
|
+
};
|
|
98
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
99
|
+
const arg = args[index];
|
|
100
|
+
if (arg === "--yes") {
|
|
101
|
+
options.yes = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--no-open") {
|
|
105
|
+
options.noOpen = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--verbose") {
|
|
109
|
+
options.verbose = true;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (arg === "--name") {
|
|
113
|
+
const value = args[index + 1];
|
|
114
|
+
if (!value || value.startsWith("--")) {
|
|
115
|
+
throw new Error("Missing value for --name.");
|
|
116
|
+
}
|
|
117
|
+
options.companyName = value.trim();
|
|
118
|
+
index += 1;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
122
|
+
}
|
|
123
|
+
if (options.yes && !options.companyName) {
|
|
124
|
+
throw new Error("`--yes` requires `--name <company>` for a non-interactive install.");
|
|
125
|
+
}
|
|
126
|
+
return options;
|
|
127
|
+
}
|
|
128
|
+
function parseClaudeArgs(args) {
|
|
129
|
+
const options = {
|
|
130
|
+
noOpen: false,
|
|
131
|
+
verbose: false,
|
|
132
|
+
yes: false
|
|
133
|
+
};
|
|
134
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
135
|
+
const arg = args[index];
|
|
136
|
+
if (arg === "--yes") {
|
|
137
|
+
options.yes = true;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (arg === "--no-open") {
|
|
141
|
+
options.noOpen = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg === "--verbose") {
|
|
145
|
+
options.verbose = true;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (arg === "--name") {
|
|
149
|
+
const value = args[index + 1];
|
|
150
|
+
if (!value || value.startsWith("--")) {
|
|
151
|
+
throw new Error("Missing value for --name.");
|
|
152
|
+
}
|
|
153
|
+
options.companyName = value.trim();
|
|
154
|
+
index += 1;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (arg === "--project") {
|
|
158
|
+
const value = args[index + 1];
|
|
159
|
+
if (!value || value.startsWith("--")) {
|
|
160
|
+
throw new Error("Missing value for --project.");
|
|
161
|
+
}
|
|
162
|
+
options.project = value.trim();
|
|
163
|
+
index += 1;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
167
|
+
}
|
|
168
|
+
if (options.project && options.companyName) {
|
|
169
|
+
throw new Error("`--project` can not be combined with `--name`.");
|
|
170
|
+
}
|
|
171
|
+
return options;
|
|
172
|
+
}
|
|
55
173
|
function toSlug(name) {
|
|
56
174
|
return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
57
175
|
}
|
|
176
|
+
function run(command2, cwd, env) {
|
|
177
|
+
execSync(command2, {
|
|
178
|
+
cwd,
|
|
179
|
+
stdio: "inherit",
|
|
180
|
+
env: env ? { ...process.env, ...env } : process.env
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function capture(command2, cwd, env) {
|
|
184
|
+
return execSync(command2, {
|
|
185
|
+
cwd,
|
|
186
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
187
|
+
encoding: "utf8"
|
|
188
|
+
}).trim();
|
|
189
|
+
}
|
|
190
|
+
function getInstallerEnv(extraEnv) {
|
|
191
|
+
return {
|
|
192
|
+
...process.env,
|
|
193
|
+
npm_config_audit: "false",
|
|
194
|
+
npm_config_fund: "false",
|
|
195
|
+
npm_config_loglevel: "error",
|
|
196
|
+
npm_config_progress: "false",
|
|
197
|
+
npm_config_update_notifier: "false",
|
|
198
|
+
PRISMA_HIDE_UPDATE_MESSAGE: "1",
|
|
199
|
+
...extraEnv
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function compareVersions(left, right) {
|
|
203
|
+
const leftParts = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
204
|
+
const rightParts = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
205
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
206
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
207
|
+
const leftValue = leftParts[index] ?? 0;
|
|
208
|
+
const rightValue = rightParts[index] ?? 0;
|
|
209
|
+
if (leftValue === rightValue) continue;
|
|
210
|
+
return leftValue > rightValue ? 1 : -1;
|
|
211
|
+
}
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
function maybePrintShowpaneUpdateMessage(currentVersion) {
|
|
215
|
+
try {
|
|
216
|
+
const latestVersion = capture("npm view showpane version", void 0, {
|
|
217
|
+
npm_config_update_notifier: "false",
|
|
218
|
+
npm_config_fund: "false",
|
|
219
|
+
npm_config_audit: "false"
|
|
220
|
+
});
|
|
221
|
+
if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
blue(`Update available: showpane ${latestVersion} is out`);
|
|
226
|
+
console.log(` ${DIM}Run: npx showpane@latest sync${RESET}`);
|
|
227
|
+
console.log();
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function normalizePathForComparison(targetPath) {
|
|
232
|
+
const normalized = path.normalize(resolve(targetPath));
|
|
233
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
234
|
+
}
|
|
235
|
+
function isShowpaneShimOnPath() {
|
|
236
|
+
const pathValue = process.env.PATH ?? "";
|
|
237
|
+
const binDir = normalizePathForComparison(SHOWPANE_BIN_DIR);
|
|
238
|
+
return pathValue.split(path.delimiter).filter(Boolean).some((entry) => normalizePathForComparison(entry) === binDir);
|
|
239
|
+
}
|
|
240
|
+
function getResumeCommand() {
|
|
241
|
+
return isShowpaneShimOnPath() ? "showpane claude" : "npx showpane claude";
|
|
242
|
+
}
|
|
243
|
+
function getResumeHint() {
|
|
244
|
+
return isShowpaneShimOnPath() ? null : `Optional: add ${SHOWPANE_BIN_DIR} to your PATH to use ${BOLD}showpane${RESET} directly.`;
|
|
245
|
+
}
|
|
246
|
+
function getCommandOutput(errorLike) {
|
|
247
|
+
const error2 = errorLike;
|
|
248
|
+
const stdout = typeof error2?.stdout === "string" ? error2.stdout : error2?.stdout?.toString() ?? "";
|
|
249
|
+
const stderr = typeof error2?.stderr === "string" ? error2.stderr : error2?.stderr?.toString() ?? "";
|
|
250
|
+
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
251
|
+
}
|
|
252
|
+
function runQuiet(command2, cwd, env) {
|
|
253
|
+
try {
|
|
254
|
+
execSync(command2, {
|
|
255
|
+
cwd,
|
|
256
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
257
|
+
encoding: "utf8",
|
|
258
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
259
|
+
maxBuffer: 20 * 1024 * 1024
|
|
260
|
+
});
|
|
261
|
+
} catch (errorLike) {
|
|
262
|
+
const output = getCommandOutput(errorLike);
|
|
263
|
+
throw new StepCommandError(
|
|
264
|
+
errorLike instanceof Error ? errorLike.message : String(errorLike),
|
|
265
|
+
output
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function runInstallerCommand(command2, cwd, env, verbose) {
|
|
270
|
+
if (verbose) {
|
|
271
|
+
run(command2, cwd, env);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
runQuiet(command2, cwd, env);
|
|
275
|
+
}
|
|
276
|
+
var activeSpinner = null;
|
|
277
|
+
function renderSpinner(label, frame, startedAt) {
|
|
278
|
+
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
|
|
279
|
+
const elapsed = elapsedSeconds > 0 ? ` ${DIM}${elapsedSeconds}s${RESET}` : "";
|
|
280
|
+
process.stdout.write(`\r ${BLUE}${frame}${RESET} ${label}...${elapsed}\x1B[K`);
|
|
281
|
+
}
|
|
282
|
+
function stopSpinner(clearLine = true) {
|
|
283
|
+
if (!activeSpinner) return;
|
|
284
|
+
clearInterval(activeSpinner.interval);
|
|
285
|
+
if (clearLine) {
|
|
286
|
+
process.stdout.write("\r\x1B[K");
|
|
287
|
+
} else {
|
|
288
|
+
process.stdout.write("\n");
|
|
289
|
+
}
|
|
290
|
+
activeSpinner = null;
|
|
291
|
+
}
|
|
292
|
+
function stepStart(label, spinner = false) {
|
|
293
|
+
stopSpinner();
|
|
294
|
+
if (!spinner) {
|
|
295
|
+
blue(label);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
299
|
+
let frameIndex = 0;
|
|
300
|
+
const startedAt = Date.now();
|
|
301
|
+
renderSpinner(label, frames[frameIndex], startedAt);
|
|
302
|
+
const interval = setInterval(() => {
|
|
303
|
+
frameIndex = (frameIndex + 1) % frames.length;
|
|
304
|
+
renderSpinner(label, frames[frameIndex], startedAt);
|
|
305
|
+
}, 80);
|
|
306
|
+
activeSpinner = {
|
|
307
|
+
interval,
|
|
308
|
+
label,
|
|
309
|
+
startedAt
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function stepSuccess(label) {
|
|
313
|
+
stopSpinner();
|
|
314
|
+
green(label);
|
|
315
|
+
}
|
|
316
|
+
function stepFailure(label, errorLike, hint) {
|
|
317
|
+
stopSpinner();
|
|
318
|
+
error(`${label} failed.`);
|
|
319
|
+
const message = errorLike instanceof Error ? errorLike.message : String(errorLike);
|
|
320
|
+
const output = errorLike instanceof StepCommandError ? errorLike.output : getCommandOutput(errorLike);
|
|
321
|
+
if (output) {
|
|
322
|
+
console.error();
|
|
323
|
+
console.error(output.trimEnd());
|
|
324
|
+
} else if (message) {
|
|
325
|
+
console.error();
|
|
326
|
+
console.error(message.trim());
|
|
327
|
+
}
|
|
328
|
+
if (hint) {
|
|
329
|
+
console.error();
|
|
330
|
+
console.error(`Hint: ${hint}`);
|
|
331
|
+
}
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
function attachSpinnerCleanup() {
|
|
335
|
+
const cleanup = () => stopSpinner();
|
|
336
|
+
process.on("exit", cleanup);
|
|
337
|
+
process.on("SIGINT", cleanup);
|
|
338
|
+
process.on("SIGTERM", cleanup);
|
|
339
|
+
}
|
|
340
|
+
attachSpinnerCleanup();
|
|
341
|
+
function openBrowser(url) {
|
|
342
|
+
const platform = process.platform;
|
|
343
|
+
const command2 = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
344
|
+
execSync(`${command2} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
345
|
+
}
|
|
346
|
+
function readJson(filePath) {
|
|
347
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
348
|
+
}
|
|
349
|
+
function writeJson(filePath, value) {
|
|
350
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
351
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
352
|
+
`);
|
|
353
|
+
}
|
|
354
|
+
function getShowpaneConfigPath() {
|
|
355
|
+
return join(SHOWPANE_HOME, "config.json");
|
|
356
|
+
}
|
|
357
|
+
function readShowpaneConfig() {
|
|
358
|
+
const configPath = getShowpaneConfigPath();
|
|
359
|
+
if (!existsSync(configPath)) {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
return readJson(configPath);
|
|
363
|
+
}
|
|
364
|
+
function writeShowpaneConfig(config) {
|
|
365
|
+
ensureDir(SHOWPANE_HOME);
|
|
366
|
+
const configPath = getShowpaneConfigPath();
|
|
367
|
+
writeJson(configPath, config);
|
|
368
|
+
chmodSync(configPath, 384);
|
|
369
|
+
}
|
|
370
|
+
function findWorkspaceRoot(startPath) {
|
|
371
|
+
let currentPath = resolve(startPath);
|
|
372
|
+
while (true) {
|
|
373
|
+
if (existsSync(join(currentPath, "package.json")) && existsSync(join(currentPath, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(currentPath))) {
|
|
374
|
+
return currentPath;
|
|
375
|
+
}
|
|
376
|
+
const parentPath = dirname(currentPath);
|
|
377
|
+
if (parentPath === currentPath) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
currentPath = parentPath;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function defaultWorkspaceEntry(projectPath, overrides) {
|
|
384
|
+
return {
|
|
385
|
+
name: basename(projectPath),
|
|
386
|
+
path: resolve(projectPath),
|
|
387
|
+
lastUsedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
deployMode: "local",
|
|
389
|
+
orgSlug: "",
|
|
390
|
+
...overrides
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
function getWorkspaceEntries(config) {
|
|
394
|
+
const workspaces = [...config.workspaces ?? []];
|
|
395
|
+
const activePath = config.app_path ? resolve(config.app_path) : null;
|
|
396
|
+
if (activePath && !workspaces.some((workspace) => normalizePathForComparison(workspace.path) === normalizePathForComparison(activePath))) {
|
|
397
|
+
workspaces.push(defaultWorkspaceEntry(activePath, {
|
|
398
|
+
deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
|
|
399
|
+
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : ""
|
|
400
|
+
}));
|
|
401
|
+
}
|
|
402
|
+
return workspaces.map((workspace) => ({
|
|
403
|
+
...workspace,
|
|
404
|
+
path: resolve(workspace.path),
|
|
405
|
+
lastUsedAt: workspace.lastUsedAt || (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
406
|
+
deployMode: workspace.deployMode || "local",
|
|
407
|
+
orgSlug: workspace.orgSlug || ""
|
|
408
|
+
})).sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
409
|
+
}
|
|
410
|
+
function setActiveWorkspace(config, workspace) {
|
|
411
|
+
config.app_path = workspace.path;
|
|
412
|
+
config.deploy_mode = workspace.deployMode;
|
|
413
|
+
config.orgSlug = workspace.orgSlug;
|
|
414
|
+
}
|
|
415
|
+
function upsertWorkspace(config, workspace, makeActive = true) {
|
|
416
|
+
const workspaces = getWorkspaceEntries(config).filter(
|
|
417
|
+
(entry) => normalizePathForComparison(entry.path) !== normalizePathForComparison(workspace.path)
|
|
418
|
+
);
|
|
419
|
+
workspaces.push(workspace);
|
|
420
|
+
config.workspaces = workspaces.sort((left, right) => right.lastUsedAt.localeCompare(left.lastUsedAt));
|
|
421
|
+
if (makeActive) {
|
|
422
|
+
setActiveWorkspace(config, workspace);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function updateWorkspaceFromConfig(config, projectPath, overrides) {
|
|
426
|
+
const workspace = defaultWorkspaceEntry(projectPath, {
|
|
427
|
+
deployMode: typeof config.deploy_mode === "string" ? config.deploy_mode : "local",
|
|
428
|
+
orgSlug: typeof config.orgSlug === "string" ? config.orgSlug : "",
|
|
429
|
+
...overrides
|
|
430
|
+
});
|
|
431
|
+
upsertWorkspace(config, workspace, true);
|
|
432
|
+
return workspace;
|
|
433
|
+
}
|
|
434
|
+
function ensureShowpaneShim() {
|
|
435
|
+
ensureDir(SHOWPANE_BIN_DIR);
|
|
436
|
+
const shellShim = join(SHOWPANE_BIN_DIR, "showpane");
|
|
437
|
+
writeFileSync(
|
|
438
|
+
shellShim,
|
|
439
|
+
'#!/bin/sh\nexec npx --yes showpane "$@"\n'
|
|
440
|
+
);
|
|
441
|
+
chmodSync(shellShim, 493);
|
|
442
|
+
if (process.platform === "win32") {
|
|
443
|
+
writeFileSync(
|
|
444
|
+
join(SHOWPANE_BIN_DIR, "showpane.cmd"),
|
|
445
|
+
"@echo off\r\nnpx --yes showpane %*\r\n"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function ensureDir(dirPath) {
|
|
450
|
+
mkdirSync(dirPath, { recursive: true });
|
|
451
|
+
}
|
|
452
|
+
function removePath(targetPath) {
|
|
453
|
+
if (!existsSync(targetPath)) return;
|
|
454
|
+
const stat = lstatSync(targetPath);
|
|
455
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
456
|
+
unlinkSync(targetPath);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
460
|
+
}
|
|
461
|
+
function copyDirContents(sourceDir, targetDir) {
|
|
462
|
+
ensureDir(targetDir);
|
|
463
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
464
|
+
cpSync(join(sourceDir, entry), join(targetDir, entry), { recursive: true });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
function getBundledScaffoldPath(scaffoldRoot, relativePath) {
|
|
468
|
+
const baseName = path.basename(relativePath);
|
|
469
|
+
const bundledBaseName = baseName.startsWith(".") ? `__dot__${baseName.slice(1)}` : baseName;
|
|
470
|
+
return join(scaffoldRoot, dirname(relativePath), bundledBaseName);
|
|
471
|
+
}
|
|
472
|
+
function copyScaffoldFiles(scaffoldRoot, projectRoot, manifest) {
|
|
473
|
+
for (const relativePath of Object.keys(manifest.files)) {
|
|
474
|
+
const sourcePath = getBundledScaffoldPath(scaffoldRoot, relativePath);
|
|
475
|
+
const targetPath = join(projectRoot, relativePath);
|
|
476
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
477
|
+
cpSync(sourcePath, targetPath);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function hashFile(filePath) {
|
|
481
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
482
|
+
}
|
|
483
|
+
function getPathSignature(filePath) {
|
|
484
|
+
if (!existsSync(filePath)) return null;
|
|
485
|
+
const stat = lstatSync(filePath);
|
|
486
|
+
if (!stat.isFile()) {
|
|
487
|
+
return "__NON_FILE__";
|
|
488
|
+
}
|
|
489
|
+
return hashFile(filePath);
|
|
490
|
+
}
|
|
491
|
+
function commandExists(command2) {
|
|
492
|
+
try {
|
|
493
|
+
if (process.platform === "win32") {
|
|
494
|
+
execSync(`where ${command2}`, { stdio: "ignore" });
|
|
495
|
+
} else {
|
|
496
|
+
execSync(`command -v ${command2}`, { stdio: "ignore", shell: "/bin/zsh" });
|
|
497
|
+
}
|
|
498
|
+
return true;
|
|
499
|
+
} catch {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
58
503
|
function findFreePort(startPort) {
|
|
59
|
-
return new Promise((
|
|
504
|
+
return new Promise((resolvePort, rejectPort) => {
|
|
60
505
|
const server = createServer();
|
|
61
506
|
server.listen(startPort, () => {
|
|
62
|
-
server.close(() =>
|
|
507
|
+
server.close(() => resolvePort(startPort));
|
|
63
508
|
});
|
|
64
509
|
server.on("error", (err) => {
|
|
65
510
|
if (err.code === "EADDRINUSE") {
|
|
66
|
-
|
|
511
|
+
resolvePort(findFreePort(startPort + 1));
|
|
67
512
|
} else {
|
|
68
|
-
|
|
513
|
+
rejectPort(err);
|
|
69
514
|
}
|
|
70
515
|
});
|
|
71
516
|
});
|
|
72
517
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
518
|
+
function getPackageRoot() {
|
|
519
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
520
|
+
}
|
|
521
|
+
function getPackageVersion(packageRoot2) {
|
|
522
|
+
const packageJson = readJson(join(packageRoot2, "package.json"));
|
|
523
|
+
return packageJson.version;
|
|
524
|
+
}
|
|
525
|
+
function getLocalBundleRoot(packageRoot2) {
|
|
526
|
+
const bundleRoot = join(packageRoot2, "bundle");
|
|
527
|
+
if (!existsSync(join(bundleRoot, "scaffold")) || !existsSync(join(bundleRoot, "toolchain"))) {
|
|
528
|
+
throw new Error("CLI bundle assets are missing. Run `npm run build` before using the local package.");
|
|
529
|
+
}
|
|
530
|
+
return bundleRoot;
|
|
531
|
+
}
|
|
532
|
+
function getScaffoldManifest(bundleRoot) {
|
|
533
|
+
return readJson(join(bundleRoot, "meta", "scaffold-manifest.json"));
|
|
534
|
+
}
|
|
535
|
+
function getToolchainVersion(bundleRoot) {
|
|
536
|
+
return readFileSync(join(bundleRoot, "toolchain", "VERSION"), "utf8").trim();
|
|
537
|
+
}
|
|
538
|
+
function getManagedFilesPath(projectRoot) {
|
|
539
|
+
return join(projectRoot, METADATA_DIRNAME, MANAGED_FILES_FILE);
|
|
540
|
+
}
|
|
541
|
+
function getProjectMetadataPath(projectRoot) {
|
|
542
|
+
return join(projectRoot, METADATA_DIRNAME, PROJECT_METADATA_FILE);
|
|
543
|
+
}
|
|
544
|
+
function writeProjectState(projectRoot, showpaneVersion, scaffoldManifest, toolchainVersion) {
|
|
545
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
546
|
+
const projectMetadata = {
|
|
547
|
+
schemaVersion: 1,
|
|
548
|
+
showpaneVersion,
|
|
549
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
550
|
+
toolchainVersion,
|
|
551
|
+
projectRoot,
|
|
552
|
+
installedAt: timestamp,
|
|
553
|
+
lastUpgradedAt: timestamp
|
|
554
|
+
};
|
|
555
|
+
writeJson(getProjectMetadataPath(projectRoot), projectMetadata);
|
|
556
|
+
writeJson(getManagedFilesPath(projectRoot), {
|
|
557
|
+
schemaVersion: 1,
|
|
558
|
+
showpaneVersion,
|
|
559
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
560
|
+
files: scaffoldManifest.files
|
|
78
561
|
});
|
|
79
562
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
563
|
+
function readManagedFiles(projectRoot) {
|
|
564
|
+
return readJson(
|
|
565
|
+
getManagedFilesPath(projectRoot)
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
function readProjectMetadata(projectRoot) {
|
|
569
|
+
return readJson(getProjectMetadataPath(projectRoot));
|
|
570
|
+
}
|
|
571
|
+
function writeUpdatedProjectState(projectRoot, previousMetadata, showpaneVersion, scaffoldManifest, toolchainVersion) {
|
|
572
|
+
writeJson(getProjectMetadataPath(projectRoot), {
|
|
573
|
+
...previousMetadata,
|
|
574
|
+
showpaneVersion,
|
|
575
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
576
|
+
toolchainVersion,
|
|
577
|
+
lastUpgradedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
578
|
+
});
|
|
579
|
+
writeJson(getManagedFilesPath(projectRoot), {
|
|
580
|
+
schemaVersion: 1,
|
|
581
|
+
showpaneVersion,
|
|
582
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
583
|
+
files: scaffoldManifest.files
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
function detectProjectRoot(explicitProjectPath) {
|
|
587
|
+
const candidatePaths = [
|
|
588
|
+
explicitProjectPath ? resolve(explicitProjectPath) : null,
|
|
589
|
+
resolve(process.cwd()),
|
|
590
|
+
resolve(process.cwd(), "app")
|
|
591
|
+
].filter(Boolean);
|
|
592
|
+
const configPath = join(SHOWPANE_HOME, "config.json");
|
|
593
|
+
if (existsSync(configPath)) {
|
|
594
|
+
const config = readJson(configPath);
|
|
595
|
+
if (config.app_path) {
|
|
596
|
+
candidatePaths.push(resolve(config.app_path));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const candidate of candidatePaths) {
|
|
600
|
+
if (existsSync(join(candidate, "package.json")) && existsSync(join(candidate, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(candidate)) && existsSync(getManagedFilesPath(candidate))) {
|
|
601
|
+
return candidate;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
throw new Error("Could not find a Showpane project root. Run the command from the project directory or pass --project <path>.");
|
|
605
|
+
}
|
|
606
|
+
function parseEnvFile(filePath) {
|
|
607
|
+
const env = {};
|
|
608
|
+
if (!existsSync(filePath)) return env;
|
|
609
|
+
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
610
|
+
const line = rawLine.trim();
|
|
611
|
+
if (!line || line.startsWith("#")) continue;
|
|
612
|
+
const separatorIndex = line.indexOf("=");
|
|
613
|
+
if (separatorIndex === -1) continue;
|
|
614
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
615
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
616
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
617
|
+
value = value.slice(1, -1);
|
|
618
|
+
}
|
|
619
|
+
env[key] = value;
|
|
620
|
+
}
|
|
621
|
+
return env;
|
|
622
|
+
}
|
|
623
|
+
function cleanupEmptyParents(projectRoot, relativePath) {
|
|
624
|
+
let currentDir = dirname(join(projectRoot, relativePath));
|
|
625
|
+
while (currentDir.startsWith(projectRoot) && currentDir !== projectRoot) {
|
|
626
|
+
if (readdirSync(currentDir).length > 0) break;
|
|
627
|
+
rmSync(currentDir, { recursive: true, force: true });
|
|
628
|
+
currentDir = dirname(currentDir);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
function buildUpgradePlan(projectRoot, currentManifest, targetManifest) {
|
|
632
|
+
const plan = {
|
|
633
|
+
additions: [],
|
|
634
|
+
updates: [],
|
|
635
|
+
deletions: [],
|
|
636
|
+
conflicts: []
|
|
637
|
+
};
|
|
638
|
+
const allPaths = /* @__PURE__ */ new Set([
|
|
639
|
+
...Object.keys(currentManifest),
|
|
640
|
+
...Object.keys(targetManifest)
|
|
641
|
+
]);
|
|
642
|
+
for (const relativePath of [...allPaths].sort()) {
|
|
643
|
+
const currentRecordedHash = currentManifest[relativePath];
|
|
644
|
+
const targetHash = targetManifest[relativePath];
|
|
645
|
+
const absolutePath = join(projectRoot, relativePath);
|
|
646
|
+
const currentHash = getPathSignature(absolutePath);
|
|
647
|
+
const existsNow = currentHash !== null;
|
|
648
|
+
const locallyModifiedManagedFile = currentRecordedHash !== void 0 && currentHash !== currentRecordedHash;
|
|
649
|
+
const collidingUnmanagedFile = currentRecordedHash === void 0 && targetHash !== void 0 && existsNow && currentHash !== targetHash;
|
|
650
|
+
if (locallyModifiedManagedFile || collidingUnmanagedFile) {
|
|
651
|
+
plan.conflicts.push(relativePath);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (targetHash === void 0) {
|
|
655
|
+
if (existsNow) {
|
|
656
|
+
plan.deletions.push(relativePath);
|
|
657
|
+
}
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
if (!existsNow) {
|
|
661
|
+
plan.additions.push(relativePath);
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (currentHash !== targetHash) {
|
|
665
|
+
plan.updates.push(relativePath);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return plan;
|
|
669
|
+
}
|
|
670
|
+
function applyUpgradePlan(projectRoot, scaffoldSource, plan) {
|
|
671
|
+
for (const relativePath of plan.deletions) {
|
|
672
|
+
removePath(join(projectRoot, relativePath));
|
|
673
|
+
cleanupEmptyParents(projectRoot, relativePath);
|
|
674
|
+
}
|
|
675
|
+
for (const relativePath of [...plan.additions, ...plan.updates]) {
|
|
676
|
+
const sourcePath = getBundledScaffoldPath(scaffoldSource, relativePath);
|
|
677
|
+
const targetPath = join(projectRoot, relativePath);
|
|
678
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
679
|
+
cpSync(sourcePath, targetPath);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
function installDependencies(projectRoot, verbose) {
|
|
683
|
+
if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
684
|
+
if (verbose === void 0) {
|
|
685
|
+
run("npm ci", projectRoot, getInstallerEnv());
|
|
686
|
+
} else {
|
|
687
|
+
runInstallerCommand("npm ci", projectRoot, getInstallerEnv(), verbose);
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
if (verbose === void 0) {
|
|
691
|
+
run("npm install", projectRoot, getInstallerEnv());
|
|
692
|
+
} else {
|
|
693
|
+
runInstallerCommand("npm install", projectRoot, getInstallerEnv(), verbose);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
function generateLocalDatabase(projectRoot, databaseUrl, verbose) {
|
|
698
|
+
const env = getInstallerEnv({
|
|
699
|
+
DATABASE_URL: databaseUrl
|
|
700
|
+
});
|
|
701
|
+
if (verbose === void 0) {
|
|
702
|
+
run("npm run prisma:db-push", projectRoot, env);
|
|
703
|
+
} else {
|
|
704
|
+
runInstallerCommand("npm run prisma:db-push", projectRoot, env, verbose);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function seedProject(projectRoot, databaseUrl, verbose) {
|
|
708
|
+
const env = getInstallerEnv({
|
|
709
|
+
DATABASE_URL: databaseUrl
|
|
710
|
+
});
|
|
711
|
+
if (verbose === void 0) {
|
|
712
|
+
run("npx tsx prisma/seed.ts", projectRoot, env);
|
|
713
|
+
} else {
|
|
714
|
+
runInstallerCommand("npx tsx prisma/seed.ts", projectRoot, env, verbose);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function maybeRunPostUpgradeSteps(projectRoot, changedPaths) {
|
|
718
|
+
const dependenciesChanged = changedPaths.some(
|
|
719
|
+
(relativePath) => ["package.json", "package-lock.json"].includes(relativePath)
|
|
720
|
+
);
|
|
721
|
+
const prismaChanged = changedPaths.some((relativePath) => relativePath.startsWith("prisma/"));
|
|
722
|
+
if (dependenciesChanged) {
|
|
723
|
+
blue("Refreshing project dependencies");
|
|
724
|
+
installDependencies(projectRoot);
|
|
725
|
+
}
|
|
726
|
+
if (prismaChanged) {
|
|
727
|
+
const env = parseEnvFile(join(projectRoot, ".env"));
|
|
728
|
+
if (env.DATABASE_URL?.startsWith("file:")) {
|
|
729
|
+
blue("Applying local SQLite schema updates");
|
|
730
|
+
run("npm run prisma:db-push", projectRoot, getInstallerEnv({
|
|
731
|
+
DATABASE_URL: env.DATABASE_URL
|
|
732
|
+
}));
|
|
733
|
+
} else {
|
|
734
|
+
blue("Refreshing Prisma client");
|
|
735
|
+
run("npm run prisma:generate", projectRoot, getInstallerEnv());
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
function tryInitializeGitRepo(projectRoot, announce = true) {
|
|
740
|
+
if (!commandExists("git")) {
|
|
741
|
+
blue("Git not found; skipped repository initialization");
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
try {
|
|
745
|
+
execSync("git init -q -b main", { cwd: projectRoot, stdio: "ignore" });
|
|
746
|
+
} catch {
|
|
747
|
+
execSync("git init -q", { cwd: projectRoot, stdio: "ignore" });
|
|
748
|
+
try {
|
|
749
|
+
execSync("git branch -M main", { cwd: projectRoot, stdio: "ignore" });
|
|
750
|
+
} catch {
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
try {
|
|
754
|
+
run("git add .", projectRoot);
|
|
755
|
+
execSync('git commit -m "Initial Showpane scaffold"', {
|
|
756
|
+
cwd: projectRoot,
|
|
757
|
+
stdio: "ignore",
|
|
758
|
+
env: {
|
|
759
|
+
...process.env,
|
|
760
|
+
GIT_AUTHOR_NAME: "Showpane",
|
|
761
|
+
GIT_AUTHOR_EMAIL: "showpane@local.invalid",
|
|
762
|
+
GIT_COMMITTER_NAME: "Showpane",
|
|
763
|
+
GIT_COMMITTER_EMAIL: "showpane@local.invalid"
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
if (announce) {
|
|
767
|
+
green("Git repository initialized");
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
if (announce) {
|
|
771
|
+
blue("Initialized git repository without an initial commit");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function installSharedSkillProjection(toolchainRoot) {
|
|
776
|
+
const sharedSource = join(toolchainRoot, "skills", "shared");
|
|
777
|
+
const sharedTarget = join(CLAUDE_SKILLS_DIR, SHOWPANE_SHARED_SKILL);
|
|
778
|
+
removePath(sharedTarget);
|
|
779
|
+
symlinkSync(
|
|
780
|
+
sharedSource,
|
|
781
|
+
sharedTarget,
|
|
782
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
function printCreateSuccessCard(projectRoot, url) {
|
|
786
|
+
const resumeCommand = getResumeCommand();
|
|
787
|
+
const resumeHint = getResumeHint();
|
|
788
|
+
console.log();
|
|
789
|
+
console.log(` ${GREEN}Showpane is ready${RESET}`);
|
|
790
|
+
console.log();
|
|
791
|
+
console.log(` ${BOLD}Project:${RESET} ${projectRoot}`);
|
|
792
|
+
console.log(` ${BOLD}App:${RESET} ${url}`);
|
|
793
|
+
console.log(` ${BOLD}Demo:${RESET} example / demo-only-password`);
|
|
794
|
+
console.log();
|
|
795
|
+
console.log(` ${BOLD}Next:${RESET}`);
|
|
796
|
+
console.log(` ${DIM}${resumeCommand}${RESET}`);
|
|
797
|
+
console.log();
|
|
798
|
+
console.log(` ${BOLD}Try:${RESET}`);
|
|
799
|
+
console.log(` ${DIM}Create a portal for my call with Acme Health${RESET}`);
|
|
800
|
+
if (resumeHint) {
|
|
801
|
+
console.log();
|
|
802
|
+
console.log(` ${DIM}${resumeHint}${RESET}`);
|
|
803
|
+
}
|
|
804
|
+
console.log();
|
|
805
|
+
}
|
|
806
|
+
function startDevServer(projectRoot, databaseUrl, noOpen, verbose) {
|
|
807
|
+
return new Promise(async (resolveStart, rejectStart) => {
|
|
808
|
+
const port = await findFreePort(3e3);
|
|
809
|
+
const url = `http://localhost:${port}`;
|
|
810
|
+
const devServer = spawn("npm", ["run", "dev"], {
|
|
811
|
+
cwd: projectRoot,
|
|
812
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
813
|
+
env: { ...process.env, PORT: String(port), DATABASE_URL: databaseUrl }
|
|
814
|
+
});
|
|
815
|
+
let ready = false;
|
|
816
|
+
let bufferedOutput = "";
|
|
817
|
+
const readyPattern = /ready in/i;
|
|
818
|
+
const handleChunk = (target) => (chunk) => {
|
|
819
|
+
const text = chunk.toString();
|
|
820
|
+
if (verbose) {
|
|
821
|
+
target.write(text);
|
|
822
|
+
} else if (ready) {
|
|
823
|
+
target.write(text);
|
|
824
|
+
} else {
|
|
825
|
+
bufferedOutput += text;
|
|
826
|
+
if (bufferedOutput.length > 1e5) {
|
|
827
|
+
bufferedOutput = bufferedOutput.slice(-1e5);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
if (!ready && readyPattern.test(text)) {
|
|
831
|
+
ready = true;
|
|
832
|
+
stepSuccess("Start app");
|
|
833
|
+
if (!noOpen) {
|
|
834
|
+
blue(`Opening ${url}`);
|
|
835
|
+
try {
|
|
836
|
+
openBrowser(url);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
resolveStart({ devServer, url });
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
devServer.stdout?.on("data", handleChunk(process.stdout));
|
|
844
|
+
devServer.stderr?.on("data", handleChunk(process.stderr));
|
|
845
|
+
devServer.on("error", (errorLike) => {
|
|
846
|
+
rejectStart(new StepCommandError(
|
|
847
|
+
errorLike instanceof Error ? errorLike.message : String(errorLike),
|
|
848
|
+
bufferedOutput.trim()
|
|
849
|
+
));
|
|
850
|
+
});
|
|
851
|
+
devServer.on("close", (code) => {
|
|
852
|
+
if (ready) {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
const message = code === null ? "Dev server exited before becoming ready." : `Dev server exited with code ${code} before becoming ready.`;
|
|
856
|
+
rejectStart(new StepCommandError(message, bufferedOutput.trim()));
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
function resolveWorkspaceSelection(config, specifier) {
|
|
861
|
+
const workspaces = getWorkspaceEntries(config);
|
|
862
|
+
if (!specifier) {
|
|
863
|
+
return workspaces;
|
|
864
|
+
}
|
|
865
|
+
const asPathRoot = findWorkspaceRoot(specifier);
|
|
866
|
+
if (asPathRoot) {
|
|
867
|
+
return [defaultWorkspaceEntry(asPathRoot)];
|
|
868
|
+
}
|
|
869
|
+
const normalizedSpecifier = normalizePathForComparison(specifier);
|
|
870
|
+
const pathMatches = workspaces.filter(
|
|
871
|
+
(workspace) => normalizePathForComparison(workspace.path) === normalizedSpecifier
|
|
872
|
+
);
|
|
873
|
+
if (pathMatches.length > 0) {
|
|
874
|
+
return pathMatches;
|
|
875
|
+
}
|
|
876
|
+
const nameMatches = workspaces.filter((workspace) => workspace.name === specifier);
|
|
877
|
+
return nameMatches;
|
|
878
|
+
}
|
|
879
|
+
async function promptWorkspaceSelection(workspaces) {
|
|
880
|
+
console.log();
|
|
881
|
+
console.log(` ${BOLD}Select a Showpane workspace${RESET}`);
|
|
882
|
+
console.log();
|
|
883
|
+
for (const [index, workspace] of workspaces.entries()) {
|
|
884
|
+
console.log(` ${index + 1}. ${workspace.name}`);
|
|
885
|
+
console.log(` ${DIM}${workspace.path}${RESET}`);
|
|
886
|
+
console.log(` ${DIM}Last used: ${workspace.lastUsedAt}${RESET}`);
|
|
887
|
+
}
|
|
888
|
+
console.log();
|
|
889
|
+
while (true) {
|
|
890
|
+
const answer = await ask(` ${BOLD}Choose a workspace [1-${workspaces.length}] or q to cancel:${RESET} `);
|
|
891
|
+
if (!answer) continue;
|
|
892
|
+
if (answer.toLowerCase() === "q") {
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
const selectedIndex = Number.parseInt(answer, 10);
|
|
896
|
+
if (Number.isInteger(selectedIndex) && selectedIndex >= 1 && selectedIndex <= workspaces.length) {
|
|
897
|
+
return workspaces[selectedIndex - 1];
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function printWorkspaceList(config) {
|
|
902
|
+
printBanner();
|
|
903
|
+
const workspaces = getWorkspaceEntries(config);
|
|
904
|
+
if (workspaces.length === 0) {
|
|
905
|
+
console.log();
|
|
906
|
+
blue("No Showpane workspaces found");
|
|
907
|
+
console.log(` ${DIM}Run: npx showpane${RESET}`);
|
|
908
|
+
console.log();
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const activePath = config.app_path ? normalizePathForComparison(config.app_path) : null;
|
|
912
|
+
console.log();
|
|
913
|
+
console.log(` ${BOLD}Showpane workspaces${RESET}`);
|
|
914
|
+
console.log();
|
|
915
|
+
for (const [index, workspace] of workspaces.entries()) {
|
|
916
|
+
const isActive = activePath !== null && normalizePathForComparison(workspace.path) === activePath;
|
|
917
|
+
const marker = isActive ? "*" : " ";
|
|
918
|
+
console.log(` ${marker} ${index + 1}. ${workspace.name}`);
|
|
919
|
+
console.log(` ${workspace.path}`);
|
|
920
|
+
console.log(` ${DIM}Last used: ${workspace.lastUsedAt}${RESET}`);
|
|
921
|
+
}
|
|
922
|
+
console.log();
|
|
923
|
+
}
|
|
924
|
+
async function openClaudeInWorkspace(workspace) {
|
|
925
|
+
if (!commandExists("claude")) {
|
|
926
|
+
throw new Error("Claude Code is not installed or not on PATH.");
|
|
927
|
+
}
|
|
928
|
+
blue(`Opening ${workspace.name} workspace`);
|
|
929
|
+
console.log(` ${DIM}${workspace.path}${RESET}`);
|
|
930
|
+
console.log();
|
|
931
|
+
await new Promise((resolveLaunch, rejectLaunch) => {
|
|
932
|
+
const child = spawn("claude", [], {
|
|
933
|
+
cwd: workspace.path,
|
|
934
|
+
stdio: "inherit",
|
|
935
|
+
env: {
|
|
936
|
+
...process.env,
|
|
937
|
+
SHOWPANE_APP_PATH: workspace.path,
|
|
938
|
+
SHOWPANE_TOOLCHAIN_DIR: CURRENT_TOOLCHAIN_LINK
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
child.on("error", rejectLaunch);
|
|
942
|
+
child.on("close", (code) => {
|
|
943
|
+
process.exit(code ?? 0);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function installSkillProjection(toolchainRoot) {
|
|
948
|
+
removePath(join(CLAUDE_SKILLS_DIR, "showpane"));
|
|
949
|
+
installSharedSkillProjection(toolchainRoot);
|
|
950
|
+
const installedSkills = [];
|
|
951
|
+
const skillsRoot = join(toolchainRoot, "skills");
|
|
952
|
+
const skillDirs = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("portal-"));
|
|
953
|
+
for (const skillDir of skillDirs) {
|
|
954
|
+
const skillMdPath = join(skillsRoot, skillDir.name, "SKILL.md");
|
|
955
|
+
if (!existsSync(skillMdPath)) continue;
|
|
956
|
+
const skillNameMatch = readFileSync(skillMdPath, "utf8").match(/^name:\s*(.+)$/m);
|
|
957
|
+
const skillName = skillNameMatch?.[1]?.trim() || skillDir.name;
|
|
958
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, skillName);
|
|
959
|
+
removePath(targetDir);
|
|
960
|
+
mkdirSync(targetDir, { recursive: true });
|
|
961
|
+
symlinkSync(skillMdPath, join(targetDir, "SKILL.md"));
|
|
962
|
+
installedSkills.push(skillName);
|
|
963
|
+
}
|
|
964
|
+
return installedSkills;
|
|
965
|
+
}
|
|
966
|
+
function syncToolchain(bundleRoot, showpaneVersion, announce = true) {
|
|
967
|
+
const sourceToolchain = join(bundleRoot, "toolchain");
|
|
968
|
+
const targetToolchain = join(TOOLCHAIN_DIR, showpaneVersion);
|
|
969
|
+
ensureDir(TOOLCHAIN_DIR);
|
|
970
|
+
ensureDir(SHOWPANE_HOME);
|
|
971
|
+
ensureDir(CLAUDE_SKILLS_DIR);
|
|
972
|
+
removePath(targetToolchain);
|
|
973
|
+
copyDirContents(sourceToolchain, targetToolchain);
|
|
974
|
+
const helperBinDir = join(SHOWPANE_HOME, "bin");
|
|
975
|
+
ensureDir(helperBinDir);
|
|
976
|
+
removePath(join(helperBinDir, "showpane-config"));
|
|
977
|
+
symlinkSync(
|
|
978
|
+
join(targetToolchain, "bin", "showpane-config"),
|
|
979
|
+
join(helperBinDir, "showpane-config")
|
|
980
|
+
);
|
|
981
|
+
removePath(CURRENT_TOOLCHAIN_LINK);
|
|
982
|
+
symlinkSync(
|
|
983
|
+
targetToolchain,
|
|
984
|
+
CURRENT_TOOLCHAIN_LINK,
|
|
985
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
986
|
+
);
|
|
987
|
+
const installedSkills = installSkillProjection(targetToolchain);
|
|
988
|
+
if (announce) {
|
|
989
|
+
green(`Toolchain synced to v${showpaneVersion}`);
|
|
990
|
+
green(`${installedSkills.length} Claude Code skills installed`);
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
installedSkills,
|
|
994
|
+
toolchainRoot: targetToolchain,
|
|
995
|
+
toolchainVersion: getToolchainVersion(bundleRoot)
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
function extractBundleForVersion(version) {
|
|
999
|
+
const tempRoot = fs.mkdtempSync(join(os.tmpdir(), "showpane-upgrade-"));
|
|
1000
|
+
const packResult = capture(`npm pack showpane@${version} --json`, tempRoot);
|
|
1001
|
+
const packJson = JSON.parse(packResult);
|
|
1002
|
+
const tarballName = packJson.at(-1)?.filename;
|
|
1003
|
+
if (!tarballName) {
|
|
1004
|
+
throw new Error(`Could not download showpane@${version}`);
|
|
1005
|
+
}
|
|
1006
|
+
run(
|
|
1007
|
+
`tar -xzf ${JSON.stringify(join(tempRoot, tarballName))} -C ${JSON.stringify(tempRoot)}`,
|
|
1008
|
+
tempRoot
|
|
1009
|
+
);
|
|
1010
|
+
return {
|
|
1011
|
+
bundleRoot: join(tempRoot, "package", "bundle"),
|
|
1012
|
+
cleanup() {
|
|
1013
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
84
1016
|
}
|
|
85
|
-
async function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
1017
|
+
async function createProject(args) {
|
|
1018
|
+
const packageRoot2 = getPackageRoot();
|
|
1019
|
+
const bundleRoot = getLocalBundleRoot(packageRoot2);
|
|
1020
|
+
const showpaneVersion = getPackageVersion(packageRoot2);
|
|
1021
|
+
const scaffoldManifest = getScaffoldManifest(bundleRoot);
|
|
1022
|
+
let options;
|
|
1023
|
+
try {
|
|
1024
|
+
options = parseCreateArgs(args);
|
|
1025
|
+
} catch (errorLike) {
|
|
1026
|
+
printBanner();
|
|
1027
|
+
console.log();
|
|
1028
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
1029
|
+
printCreateUsage();
|
|
1030
|
+
process.exit(1);
|
|
91
1031
|
}
|
|
92
1032
|
printBanner();
|
|
93
|
-
|
|
1033
|
+
ensureShowpaneShim();
|
|
1034
|
+
const companyName = options.companyName ?? await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
94
1035
|
if (!companyName) {
|
|
95
1036
|
error("Company name is required.");
|
|
96
1037
|
process.exit(1);
|
|
97
1038
|
}
|
|
98
1039
|
const slug = toSlug(companyName);
|
|
99
1040
|
const dirName = `showpane-${slug}`;
|
|
1041
|
+
const projectRoot = resolve(process.cwd(), dirName);
|
|
1042
|
+
if (existsSync(projectRoot)) {
|
|
1043
|
+
error(`Target directory already exists: ${dirName}/`);
|
|
1044
|
+
process.exit(1);
|
|
1045
|
+
}
|
|
100
1046
|
console.log();
|
|
101
1047
|
blue(`Setting up ${BOLD}${companyName}${RESET} portal as ${DIM}${dirName}/${RESET}`);
|
|
102
1048
|
console.log();
|
|
1049
|
+
stepStart("Create project");
|
|
103
1050
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch {
|
|
109
|
-
error("Failed to clone repository. Check your internet connection and try again.");
|
|
110
|
-
process.exit(1);
|
|
1051
|
+
copyScaffoldFiles(join(bundleRoot, "scaffold"), projectRoot, scaffoldManifest);
|
|
1052
|
+
stepSuccess("Project created");
|
|
1053
|
+
} catch (errorLike) {
|
|
1054
|
+
stepFailure("Create project", errorLike);
|
|
111
1055
|
}
|
|
112
|
-
|
|
1056
|
+
stepStart("Install dependencies");
|
|
113
1057
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} catch {
|
|
117
|
-
|
|
118
|
-
|
|
1058
|
+
installDependencies(projectRoot, options.verbose);
|
|
1059
|
+
stepSuccess("Dependencies installed");
|
|
1060
|
+
} catch (errorLike) {
|
|
1061
|
+
stepFailure(
|
|
1062
|
+
"Install dependencies",
|
|
1063
|
+
errorLike,
|
|
1064
|
+
"Check your Node.js version and network connection, then try again."
|
|
1065
|
+
);
|
|
119
1066
|
}
|
|
120
1067
|
const authSecret = randomBytes(32).toString("hex");
|
|
121
1068
|
const databaseUrl = "file:./dev.db";
|
|
122
|
-
|
|
1069
|
+
writeFileSync(
|
|
1070
|
+
join(projectRoot, ".env"),
|
|
1071
|
+
`DATABASE_URL="${databaseUrl}"
|
|
123
1072
|
AUTH_SECRET="${authSecret}"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
1073
|
+
`
|
|
1074
|
+
);
|
|
1075
|
+
stepStart("Configure database");
|
|
127
1076
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
1077
|
+
generateLocalDatabase(projectRoot, databaseUrl, options.verbose);
|
|
1078
|
+
seedProject(projectRoot, databaseUrl, options.verbose);
|
|
1079
|
+
stepSuccess("Database configured");
|
|
1080
|
+
} catch (errorLike) {
|
|
1081
|
+
stepFailure(
|
|
1082
|
+
"Configure database",
|
|
1083
|
+
errorLike,
|
|
1084
|
+
"Check Prisma setup and the generated .env file, then retry the install."
|
|
1085
|
+
);
|
|
135
1086
|
}
|
|
1087
|
+
stepStart("Install Claude skills");
|
|
1088
|
+
let toolchainInfo;
|
|
136
1089
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
1090
|
+
toolchainInfo = syncToolchain(bundleRoot, showpaneVersion, false);
|
|
1091
|
+
const config = readShowpaneConfig();
|
|
1092
|
+
updateWorkspaceFromConfig(config, projectRoot, {
|
|
1093
|
+
name: dirName,
|
|
1094
|
+
deployMode: "local",
|
|
1095
|
+
orgSlug: ""
|
|
139
1096
|
});
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
1097
|
+
writeShowpaneConfig(config);
|
|
1098
|
+
writeProjectState(
|
|
1099
|
+
projectRoot,
|
|
1100
|
+
showpaneVersion,
|
|
1101
|
+
scaffoldManifest,
|
|
1102
|
+
toolchainInfo.toolchainVersion
|
|
1103
|
+
);
|
|
1104
|
+
tryInitializeGitRepo(projectRoot, false);
|
|
1105
|
+
stepSuccess("Claude skills installed");
|
|
1106
|
+
} catch (errorLike) {
|
|
1107
|
+
stepFailure(
|
|
1108
|
+
"Install Claude skills",
|
|
1109
|
+
errorLike,
|
|
1110
|
+
"Check permissions for ~/.showpane and ~/.claude/skills, then try again."
|
|
1111
|
+
);
|
|
143
1112
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const oldSymlink = path.join(skillsTarget, "showpane");
|
|
1113
|
+
stepStart("Start app");
|
|
1114
|
+
let serverStart;
|
|
147
1115
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const skillName = nameMatch?.[1]?.trim() || dir.name;
|
|
161
|
-
const targetDir = path.join(skillsTarget, skillName);
|
|
162
|
-
try {
|
|
163
|
-
const stat = fs.lstatSync(targetDir);
|
|
164
|
-
if (stat.isSymbolicLink()) fs.unlinkSync(targetDir);
|
|
165
|
-
} catch {
|
|
166
|
-
}
|
|
167
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
168
|
-
const skillFile = path.join(targetDir, "SKILL.md");
|
|
169
|
-
try {
|
|
170
|
-
fs.unlinkSync(skillFile);
|
|
171
|
-
} catch {
|
|
172
|
-
}
|
|
173
|
-
fs.symlinkSync(skillMdPath, skillFile);
|
|
174
|
-
installedSkills.push(skillName);
|
|
175
|
-
const prefixedDir = path.join(skillsTarget, `showpane-${skillName}`);
|
|
176
|
-
try {
|
|
177
|
-
if (fs.existsSync(prefixedDir) && fs.lstatSync(path.join(prefixedDir, "SKILL.md")).isSymbolicLink()) {
|
|
178
|
-
const linkDest = fs.readlinkSync(path.join(prefixedDir, "SKILL.md"));
|
|
179
|
-
if (linkDest.includes("showpane") || linkDest.includes("portal")) {
|
|
180
|
-
fs.rmSync(prefixedDir, { recursive: true });
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
1116
|
+
serverStart = await startDevServer(
|
|
1117
|
+
projectRoot,
|
|
1118
|
+
databaseUrl,
|
|
1119
|
+
options.noOpen,
|
|
1120
|
+
options.verbose
|
|
1121
|
+
);
|
|
1122
|
+
} catch (errorLike) {
|
|
1123
|
+
stepFailure(
|
|
1124
|
+
"Start app",
|
|
1125
|
+
errorLike,
|
|
1126
|
+
`Run ${BOLD}cd ${dirName} && npm run dev${RESET} for more detail.`
|
|
1127
|
+
);
|
|
185
1128
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
green(`Server starting on port ${port}`);
|
|
189
|
-
const url = `http://localhost:${port}`;
|
|
190
|
-
blue(`Opening ${url}`);
|
|
191
|
-
console.log();
|
|
192
|
-
console.log(` ${GREEN}Ready!${RESET} ${installedSkills.length} Claude Code skills installed.`);
|
|
193
|
-
console.log();
|
|
194
|
-
console.log(` Open Claude Code and create your first portal:`);
|
|
195
|
-
console.log(` ${DIM}cd ${dirName}/app${RESET}`);
|
|
196
|
-
console.log(` ${BOLD}claude${RESET}`);
|
|
197
|
-
console.log(` ${DIM}> Create a portal for my call with [client name]${RESET}`);
|
|
198
|
-
console.log();
|
|
199
|
-
console.log(` Available skills: /portal-create, /portal-deploy, /portal-list, ...`);
|
|
200
|
-
console.log();
|
|
201
|
-
console.log(` ${DIM}Don't have Claude Code? Install from https://claude.ai/code${RESET}`);
|
|
202
|
-
console.log();
|
|
203
|
-
const devServer = spawn("npm", ["run", "dev"], {
|
|
204
|
-
cwd: appDir,
|
|
205
|
-
stdio: "inherit",
|
|
206
|
-
env: { ...process.env, PORT: String(port), DATABASE_URL: databaseUrl }
|
|
207
|
-
});
|
|
208
|
-
setTimeout(() => {
|
|
209
|
-
openBrowser(url);
|
|
210
|
-
}, 3e3);
|
|
211
|
-
devServer.on("close", (code) => {
|
|
1129
|
+
printCreateSuccessCard(projectRoot, serverStart.url);
|
|
1130
|
+
serverStart.devServer.on("close", (code) => {
|
|
212
1131
|
if (code !== 0) {
|
|
213
1132
|
error(`Dev server exited with code ${code}`);
|
|
214
1133
|
}
|
|
215
1134
|
process.exit(code ?? 1);
|
|
216
1135
|
});
|
|
217
1136
|
process.on("SIGINT", () => {
|
|
218
|
-
devServer.kill("SIGINT");
|
|
1137
|
+
serverStart.devServer.kill("SIGINT");
|
|
219
1138
|
});
|
|
220
1139
|
process.on("SIGTERM", () => {
|
|
221
|
-
devServer.kill("SIGTERM");
|
|
1140
|
+
serverStart.devServer.kill("SIGTERM");
|
|
222
1141
|
});
|
|
223
1142
|
}
|
|
224
|
-
|
|
1143
|
+
async function syncCurrentToolchain() {
|
|
1144
|
+
const packageRoot2 = getPackageRoot();
|
|
1145
|
+
const bundleRoot = getLocalBundleRoot(packageRoot2);
|
|
1146
|
+
const showpaneVersion = getPackageVersion(packageRoot2);
|
|
1147
|
+
ensureShowpaneShim();
|
|
1148
|
+
printBanner();
|
|
1149
|
+
maybePrintShowpaneUpdateMessage(showpaneVersion);
|
|
1150
|
+
console.log();
|
|
1151
|
+
blue(`Syncing Showpane toolchain v${showpaneVersion}`);
|
|
1152
|
+
console.log();
|
|
1153
|
+
syncToolchain(bundleRoot, showpaneVersion);
|
|
1154
|
+
}
|
|
1155
|
+
function parseUpgradeArgs(args) {
|
|
1156
|
+
const getArg = (flag) => {
|
|
1157
|
+
const index = args.indexOf(flag);
|
|
1158
|
+
return index !== -1 ? args[index + 1] : void 0;
|
|
1159
|
+
};
|
|
1160
|
+
return {
|
|
1161
|
+
targetVersion: getArg("--to"),
|
|
1162
|
+
projectPath: getArg("--project"),
|
|
1163
|
+
dryRun: args.includes("--dry-run")
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
async function upgradeProject(args) {
|
|
1167
|
+
const packageRoot2 = getPackageRoot();
|
|
1168
|
+
const currentCliVersion = getPackageVersion(packageRoot2);
|
|
1169
|
+
const { targetVersion, projectPath, dryRun } = parseUpgradeArgs(args);
|
|
1170
|
+
const resolvedTargetVersion = targetVersion || currentCliVersion;
|
|
1171
|
+
const projectRoot = detectProjectRoot(projectPath);
|
|
1172
|
+
printBanner();
|
|
1173
|
+
maybePrintShowpaneUpdateMessage(currentCliVersion);
|
|
1174
|
+
console.log();
|
|
1175
|
+
blue(`Preparing upgrade for ${DIM}${projectRoot}${RESET}`);
|
|
1176
|
+
console.log();
|
|
1177
|
+
const bundleSource = resolvedTargetVersion === currentCliVersion ? { bundleRoot: getLocalBundleRoot(packageRoot2), cleanup() {
|
|
1178
|
+
} } : extractBundleForVersion(resolvedTargetVersion);
|
|
1179
|
+
try {
|
|
1180
|
+
const targetBundleRoot = bundleSource.bundleRoot;
|
|
1181
|
+
const scaffoldManifest = getScaffoldManifest(targetBundleRoot);
|
|
1182
|
+
const currentManagedFiles = readManagedFiles(projectRoot).files;
|
|
1183
|
+
const projectMetadata = readProjectMetadata(projectRoot);
|
|
1184
|
+
const plan = buildUpgradePlan(projectRoot, currentManagedFiles, scaffoldManifest.files);
|
|
1185
|
+
if (plan.conflicts.length > 0) {
|
|
1186
|
+
error(`Upgrade blocked by ${plan.conflicts.length} modified managed file(s).`);
|
|
1187
|
+
for (const relativePath of plan.conflicts) {
|
|
1188
|
+
console.error(` ${relativePath}`);
|
|
1189
|
+
}
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1192
|
+
console.log(` Additions: ${plan.additions.length}`);
|
|
1193
|
+
console.log(` Updates: ${plan.updates.length}`);
|
|
1194
|
+
console.log(` Deletions: ${plan.deletions.length}`);
|
|
1195
|
+
console.log(` Conflicts: ${plan.conflicts.length}`);
|
|
1196
|
+
console.log();
|
|
1197
|
+
if (dryRun) {
|
|
1198
|
+
green(`Dry run complete for showpane@${resolvedTargetVersion}`);
|
|
1199
|
+
process.exit(0);
|
|
1200
|
+
}
|
|
1201
|
+
applyUpgradePlan(projectRoot, join(targetBundleRoot, "scaffold"), plan);
|
|
1202
|
+
maybeRunPostUpgradeSteps(projectRoot, [
|
|
1203
|
+
...plan.additions,
|
|
1204
|
+
...plan.updates,
|
|
1205
|
+
...plan.deletions
|
|
1206
|
+
]);
|
|
1207
|
+
const toolchainInfo = syncToolchain(targetBundleRoot, resolvedTargetVersion);
|
|
1208
|
+
writeUpdatedProjectState(
|
|
1209
|
+
projectRoot,
|
|
1210
|
+
projectMetadata,
|
|
1211
|
+
resolvedTargetVersion,
|
|
1212
|
+
scaffoldManifest,
|
|
1213
|
+
toolchainInfo.toolchainVersion
|
|
1214
|
+
);
|
|
1215
|
+
green(`Project upgraded to showpane@${resolvedTargetVersion}`);
|
|
1216
|
+
} finally {
|
|
1217
|
+
bundleSource.cleanup();
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function openClaude(args) {
|
|
1221
|
+
let options;
|
|
1222
|
+
try {
|
|
1223
|
+
options = parseClaudeArgs(args);
|
|
1224
|
+
} catch (errorLike) {
|
|
1225
|
+
printBanner();
|
|
1226
|
+
console.log();
|
|
1227
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
1228
|
+
printClaudeUsage();
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
ensureShowpaneShim();
|
|
1232
|
+
const config = readShowpaneConfig();
|
|
1233
|
+
const workspaces = getWorkspaceEntries(config);
|
|
1234
|
+
if (workspaces.length === 0 && !options.project) {
|
|
1235
|
+
blue("No Showpane workspace found. Let's create one first.");
|
|
1236
|
+
console.log();
|
|
1237
|
+
await createProject(args);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (workspaces.length > 0 && (options.companyName || options.yes)) {
|
|
1241
|
+
error("`--yes` and `--name` only apply when creating the first workspace.");
|
|
1242
|
+
printClaudeUsage();
|
|
1243
|
+
process.exit(1);
|
|
1244
|
+
}
|
|
1245
|
+
let workspace;
|
|
1246
|
+
if (options.project) {
|
|
1247
|
+
const matches = resolveWorkspaceSelection(config, options.project);
|
|
1248
|
+
if (matches.length === 0) {
|
|
1249
|
+
error(`Could not find a Showpane workspace matching: ${options.project}`);
|
|
1250
|
+
process.exit(1);
|
|
1251
|
+
}
|
|
1252
|
+
if (matches.length > 1) {
|
|
1253
|
+
error(`Multiple workspaces matched: ${options.project}`);
|
|
1254
|
+
for (const match of matches) {
|
|
1255
|
+
console.error(` ${match.name} \u2014 ${match.path}`);
|
|
1256
|
+
}
|
|
1257
|
+
process.exit(1);
|
|
1258
|
+
}
|
|
1259
|
+
workspace = matches[0];
|
|
1260
|
+
} else if (workspaces.length === 1) {
|
|
1261
|
+
workspace = workspaces[0];
|
|
1262
|
+
} else if (!process.stdout.isTTY) {
|
|
1263
|
+
error("Multiple Showpane workspaces found. Use `showpane claude --project <name-or-path>`.");
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
} else {
|
|
1266
|
+
workspace = await promptWorkspaceSelection(workspaces);
|
|
1267
|
+
}
|
|
1268
|
+
const workspaceRoot = findWorkspaceRoot(workspace.path) ?? resolve(workspace.path);
|
|
1269
|
+
const selectedWorkspace = defaultWorkspaceEntry(workspaceRoot, {
|
|
1270
|
+
name: workspace.name || basename(workspaceRoot),
|
|
1271
|
+
deployMode: workspace.deployMode || "local",
|
|
1272
|
+
orgSlug: workspace.orgSlug || ""
|
|
1273
|
+
});
|
|
1274
|
+
upsertWorkspace(config, selectedWorkspace, true);
|
|
1275
|
+
writeShowpaneConfig(config);
|
|
1276
|
+
try {
|
|
1277
|
+
await openClaudeInWorkspace(selectedWorkspace);
|
|
1278
|
+
} catch (errorLike) {
|
|
1279
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
1280
|
+
console.error(`Hint: Install Claude Code first, or use ${getResumeCommand()} later.`);
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
225
1284
|
async function login() {
|
|
226
1285
|
printBanner();
|
|
1286
|
+
ensureShowpaneShim();
|
|
227
1287
|
blue("Authenticating with Showpane...");
|
|
228
1288
|
console.log();
|
|
229
1289
|
const initRes = await fetch(`${API_BASE}/api/cli/init`, { method: "POST" });
|
|
@@ -237,11 +1297,11 @@ async function login() {
|
|
|
237
1297
|
blue(`Opened ${verificationUrl}`);
|
|
238
1298
|
console.log();
|
|
239
1299
|
blue("Waiting for authorization...");
|
|
240
|
-
const
|
|
241
|
-
const
|
|
1300
|
+
const pollInterval = 2e3;
|
|
1301
|
+
const timeoutMs = 10 * 60 * 1e3;
|
|
242
1302
|
const start = Date.now();
|
|
243
|
-
while (Date.now() - start <
|
|
244
|
-
await new Promise((
|
|
1303
|
+
while (Date.now() - start < timeoutMs) {
|
|
1304
|
+
await new Promise((resolveLater) => setTimeout(resolveLater, pollInterval));
|
|
245
1305
|
const pollRes = await fetch(`${API_BASE}/api/cli/poll?code=${code}`);
|
|
246
1306
|
if (pollRes.status === 410) {
|
|
247
1307
|
error("Code expired. Please try again.");
|
|
@@ -251,43 +1311,67 @@ async function login() {
|
|
|
251
1311
|
throw new Error(`Poll failed (${pollRes.status})`);
|
|
252
1312
|
}
|
|
253
1313
|
const data = await pollRes.json();
|
|
254
|
-
if (data.status
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
null,
|
|
271
|
-
2
|
|
272
|
-
)
|
|
273
|
-
);
|
|
274
|
-
chmodSync(configPath, 384);
|
|
275
|
-
console.log();
|
|
276
|
-
green(`Authenticated! Connected to ${BOLD}${data.orgSlug}${RESET}`);
|
|
277
|
-
console.log();
|
|
278
|
-
return;
|
|
1314
|
+
if (data.status !== "approved") continue;
|
|
1315
|
+
const config = readShowpaneConfig();
|
|
1316
|
+
config.accessToken = data.accessToken;
|
|
1317
|
+
config.accessTokenExpiresAt = data.tokenExpiresAt;
|
|
1318
|
+
config.orgSlug = data.orgSlug;
|
|
1319
|
+
config.portalUrl = data.portalUrl;
|
|
1320
|
+
config.vercelProjectId = data.vercelProjectId;
|
|
1321
|
+
const currentWorkspace = findWorkspaceRoot(process.cwd()) ?? (config.app_path ? findWorkspaceRoot(config.app_path) ?? resolve(config.app_path) : null);
|
|
1322
|
+
if (currentWorkspace) {
|
|
1323
|
+
updateWorkspaceFromConfig(config, currentWorkspace, {
|
|
1324
|
+
name: basename(currentWorkspace),
|
|
1325
|
+
deployMode: "cloud",
|
|
1326
|
+
orgSlug: data.orgSlug
|
|
1327
|
+
});
|
|
1328
|
+
} else {
|
|
1329
|
+
config.deploy_mode = "cloud";
|
|
279
1330
|
}
|
|
1331
|
+
writeShowpaneConfig(config);
|
|
1332
|
+
console.log();
|
|
1333
|
+
green(`Authenticated! Connected to ${BOLD}${data.orgSlug}${RESET}`);
|
|
1334
|
+
console.log();
|
|
1335
|
+
return;
|
|
280
1336
|
}
|
|
281
1337
|
error("Authentication timed out. Please try again.");
|
|
282
1338
|
process.exit(1);
|
|
283
1339
|
}
|
|
284
|
-
|
|
1340
|
+
var command = process.argv[2];
|
|
1341
|
+
var packageRoot = getPackageRoot();
|
|
1342
|
+
if (process.argv.includes("--version")) {
|
|
1343
|
+
console.log(getPackageVersion(packageRoot));
|
|
1344
|
+
process.exit(0);
|
|
1345
|
+
}
|
|
1346
|
+
if (command === "login") {
|
|
285
1347
|
login().catch((err) => {
|
|
286
1348
|
error(String(err));
|
|
287
1349
|
process.exit(1);
|
|
288
1350
|
});
|
|
1351
|
+
} else if (command === "claude") {
|
|
1352
|
+
openClaude(process.argv.slice(3)).catch((err) => {
|
|
1353
|
+
error(String(err));
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
});
|
|
1356
|
+
} else if (command === "projects") {
|
|
1357
|
+
try {
|
|
1358
|
+
printWorkspaceList(readShowpaneConfig());
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
error(String(err));
|
|
1361
|
+
process.exit(1);
|
|
1362
|
+
}
|
|
1363
|
+
} else if (command === "sync") {
|
|
1364
|
+
syncCurrentToolchain().catch((err) => {
|
|
1365
|
+
error(String(err));
|
|
1366
|
+
process.exit(1);
|
|
1367
|
+
});
|
|
1368
|
+
} else if (command === "upgrade") {
|
|
1369
|
+
upgradeProject(process.argv.slice(3)).catch((err) => {
|
|
1370
|
+
error(String(err));
|
|
1371
|
+
process.exit(1);
|
|
1372
|
+
});
|
|
289
1373
|
} else {
|
|
290
|
-
|
|
1374
|
+
createProject(process.argv.slice(2)).catch((err) => {
|
|
291
1375
|
error(String(err));
|
|
292
1376
|
process.exit(1);
|
|
293
1377
|
});
|