showpane 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -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 +171 -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 +873 -166
- 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 { dirname, join, resolve } = path;
|
|
14
27
|
var { homedir } = os;
|
|
15
28
|
var RESET = "\x1B[0m";
|
|
16
29
|
var BOLD = "\x1B[1m";
|
|
@@ -19,14 +32,34 @@ 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 TOOLCHAIN_DIR = join(SHOWPANE_HOME, "toolchains");
|
|
38
|
+
var CURRENT_TOOLCHAIN_LINK = join(SHOWPANE_HOME, "current");
|
|
39
|
+
var CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
40
|
+
var SHOWPANE_SHARED_SKILL = "showpane-shared";
|
|
41
|
+
var METADATA_DIRNAME = ".showpane";
|
|
42
|
+
var PROJECT_METADATA_FILE = "project.json";
|
|
43
|
+
var MANAGED_FILES_FILE = "managed-files.json";
|
|
44
|
+
var StepCommandError = class extends Error {
|
|
45
|
+
output;
|
|
46
|
+
constructor(message, output = "") {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "StepCommandError";
|
|
49
|
+
this.output = output;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
function green(message) {
|
|
53
|
+
console.log(` ${GREEN}\u2713${RESET} ${message}`);
|
|
24
54
|
}
|
|
25
|
-
function blue(
|
|
26
|
-
console.log(` ${BLUE}\u2192${RESET} ${
|
|
55
|
+
function blue(message) {
|
|
56
|
+
console.log(` ${BLUE}\u2192${RESET} ${message}`);
|
|
27
57
|
}
|
|
28
|
-
function error(
|
|
29
|
-
console.error(` ${RED}\u2717${RESET} ${
|
|
58
|
+
function error(message) {
|
|
59
|
+
console.error(` ${RED}\u2717${RESET} ${message}`);
|
|
60
|
+
}
|
|
61
|
+
function printCreateUsage() {
|
|
62
|
+
console.log("Usage: showpane [--yes --name <company>] [--no-open] [--verbose]");
|
|
30
63
|
}
|
|
31
64
|
function printBanner() {
|
|
32
65
|
const banner = `
|
|
@@ -36,7 +69,7 @@ ${BOLD}${WHITE} \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2
|
|
|
36
69
|
\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
70
|
\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
71
|
\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}
|
|
72
|
+
${DIM} AI-powered client portals.${RESET}
|
|
40
73
|
`;
|
|
41
74
|
console.log(banner);
|
|
42
75
|
}
|
|
@@ -45,183 +78,842 @@ function ask(question) {
|
|
|
45
78
|
input: process.stdin,
|
|
46
79
|
output: process.stdout
|
|
47
80
|
});
|
|
48
|
-
return new Promise((
|
|
81
|
+
return new Promise((resolveAnswer) => {
|
|
49
82
|
rl.question(question, (answer) => {
|
|
50
83
|
rl.close();
|
|
51
|
-
|
|
84
|
+
resolveAnswer(answer.trim());
|
|
52
85
|
});
|
|
53
86
|
});
|
|
54
87
|
}
|
|
88
|
+
function parseCreateArgs(args) {
|
|
89
|
+
const options = {
|
|
90
|
+
noOpen: false,
|
|
91
|
+
verbose: false,
|
|
92
|
+
yes: false
|
|
93
|
+
};
|
|
94
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
95
|
+
const arg = args[index];
|
|
96
|
+
if (arg === "--yes") {
|
|
97
|
+
options.yes = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === "--no-open") {
|
|
101
|
+
options.noOpen = true;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--verbose") {
|
|
105
|
+
options.verbose = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--name") {
|
|
109
|
+
const value = args[index + 1];
|
|
110
|
+
if (!value || value.startsWith("--")) {
|
|
111
|
+
throw new Error("Missing value for --name.");
|
|
112
|
+
}
|
|
113
|
+
options.companyName = value.trim();
|
|
114
|
+
index += 1;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
118
|
+
}
|
|
119
|
+
if (options.yes && !options.companyName) {
|
|
120
|
+
throw new Error("`--yes` requires `--name <company>` for a non-interactive install.");
|
|
121
|
+
}
|
|
122
|
+
return options;
|
|
123
|
+
}
|
|
55
124
|
function toSlug(name) {
|
|
56
125
|
return name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
57
126
|
}
|
|
127
|
+
function run(command2, cwd, env) {
|
|
128
|
+
execSync(command2, {
|
|
129
|
+
cwd,
|
|
130
|
+
stdio: "inherit",
|
|
131
|
+
env: env ? { ...process.env, ...env } : process.env
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
function capture(command2, cwd, env) {
|
|
135
|
+
return execSync(command2, {
|
|
136
|
+
cwd,
|
|
137
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
138
|
+
encoding: "utf8"
|
|
139
|
+
}).trim();
|
|
140
|
+
}
|
|
141
|
+
function getInstallerEnv(extraEnv) {
|
|
142
|
+
return {
|
|
143
|
+
...process.env,
|
|
144
|
+
npm_config_audit: "false",
|
|
145
|
+
npm_config_fund: "false",
|
|
146
|
+
npm_config_loglevel: "error",
|
|
147
|
+
npm_config_progress: "false",
|
|
148
|
+
npm_config_update_notifier: "false",
|
|
149
|
+
PRISMA_HIDE_UPDATE_MESSAGE: "1",
|
|
150
|
+
...extraEnv
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function compareVersions(left, right) {
|
|
154
|
+
const leftParts = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
155
|
+
const rightParts = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
156
|
+
const maxLength = Math.max(leftParts.length, rightParts.length);
|
|
157
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
158
|
+
const leftValue = leftParts[index] ?? 0;
|
|
159
|
+
const rightValue = rightParts[index] ?? 0;
|
|
160
|
+
if (leftValue === rightValue) continue;
|
|
161
|
+
return leftValue > rightValue ? 1 : -1;
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
function maybePrintShowpaneUpdateMessage(currentVersion) {
|
|
166
|
+
try {
|
|
167
|
+
const latestVersion = capture("npm view showpane version", void 0, {
|
|
168
|
+
npm_config_update_notifier: "false",
|
|
169
|
+
npm_config_fund: "false",
|
|
170
|
+
npm_config_audit: "false"
|
|
171
|
+
});
|
|
172
|
+
if (!latestVersion || compareVersions(latestVersion, currentVersion) <= 0) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log();
|
|
176
|
+
blue(`Update available: showpane ${latestVersion} is out`);
|
|
177
|
+
console.log(` ${DIM}Run: npx showpane@latest sync${RESET}`);
|
|
178
|
+
console.log();
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function getCommandOutput(errorLike) {
|
|
183
|
+
const error2 = errorLike;
|
|
184
|
+
const stdout = typeof error2?.stdout === "string" ? error2.stdout : error2?.stdout?.toString() ?? "";
|
|
185
|
+
const stderr = typeof error2?.stderr === "string" ? error2.stderr : error2?.stderr?.toString() ?? "";
|
|
186
|
+
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
187
|
+
}
|
|
188
|
+
function runQuiet(command2, cwd, env) {
|
|
189
|
+
try {
|
|
190
|
+
execSync(command2, {
|
|
191
|
+
cwd,
|
|
192
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
193
|
+
encoding: "utf8",
|
|
194
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
195
|
+
maxBuffer: 20 * 1024 * 1024
|
|
196
|
+
});
|
|
197
|
+
} catch (errorLike) {
|
|
198
|
+
const output = getCommandOutput(errorLike);
|
|
199
|
+
throw new StepCommandError(
|
|
200
|
+
errorLike instanceof Error ? errorLike.message : String(errorLike),
|
|
201
|
+
output
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function runInstallerCommand(command2, cwd, env, verbose) {
|
|
206
|
+
if (verbose) {
|
|
207
|
+
run(command2, cwd, env);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
runQuiet(command2, cwd, env);
|
|
211
|
+
}
|
|
212
|
+
function stepStart(label) {
|
|
213
|
+
blue(label);
|
|
214
|
+
}
|
|
215
|
+
function stepSuccess(label) {
|
|
216
|
+
green(label);
|
|
217
|
+
}
|
|
218
|
+
function stepFailure(label, errorLike, hint) {
|
|
219
|
+
error(`${label} failed.`);
|
|
220
|
+
const message = errorLike instanceof Error ? errorLike.message : String(errorLike);
|
|
221
|
+
const output = errorLike instanceof StepCommandError ? errorLike.output : getCommandOutput(errorLike);
|
|
222
|
+
if (output) {
|
|
223
|
+
console.error();
|
|
224
|
+
console.error(output.trimEnd());
|
|
225
|
+
} else if (message) {
|
|
226
|
+
console.error();
|
|
227
|
+
console.error(message.trim());
|
|
228
|
+
}
|
|
229
|
+
if (hint) {
|
|
230
|
+
console.error();
|
|
231
|
+
console.error(`Hint: ${hint}`);
|
|
232
|
+
}
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
function openBrowser(url) {
|
|
236
|
+
const platform = process.platform;
|
|
237
|
+
const command2 = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
238
|
+
execSync(`${command2} ${JSON.stringify(url)}`, { stdio: "ignore" });
|
|
239
|
+
}
|
|
240
|
+
function readJson(filePath) {
|
|
241
|
+
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
242
|
+
}
|
|
243
|
+
function writeJson(filePath, value) {
|
|
244
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
245
|
+
writeFileSync(filePath, `${JSON.stringify(value, null, 2)}
|
|
246
|
+
`);
|
|
247
|
+
}
|
|
248
|
+
function ensureDir(dirPath) {
|
|
249
|
+
mkdirSync(dirPath, { recursive: true });
|
|
250
|
+
}
|
|
251
|
+
function removePath(targetPath) {
|
|
252
|
+
if (!existsSync(targetPath)) return;
|
|
253
|
+
const stat = lstatSync(targetPath);
|
|
254
|
+
if (stat.isSymbolicLink() || stat.isFile()) {
|
|
255
|
+
unlinkSync(targetPath);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
function copyDirContents(sourceDir, targetDir) {
|
|
261
|
+
ensureDir(targetDir);
|
|
262
|
+
for (const entry of readdirSync(sourceDir)) {
|
|
263
|
+
cpSync(join(sourceDir, entry), join(targetDir, entry), { recursive: true });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function getBundledScaffoldPath(scaffoldRoot, relativePath) {
|
|
267
|
+
const baseName = path.basename(relativePath);
|
|
268
|
+
const bundledBaseName = baseName.startsWith(".") ? `__dot__${baseName.slice(1)}` : baseName;
|
|
269
|
+
return join(scaffoldRoot, dirname(relativePath), bundledBaseName);
|
|
270
|
+
}
|
|
271
|
+
function copyScaffoldFiles(scaffoldRoot, projectRoot, manifest) {
|
|
272
|
+
for (const relativePath of Object.keys(manifest.files)) {
|
|
273
|
+
const sourcePath = getBundledScaffoldPath(scaffoldRoot, relativePath);
|
|
274
|
+
const targetPath = join(projectRoot, relativePath);
|
|
275
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
276
|
+
cpSync(sourcePath, targetPath);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function hashFile(filePath) {
|
|
280
|
+
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
281
|
+
}
|
|
282
|
+
function getPathSignature(filePath) {
|
|
283
|
+
if (!existsSync(filePath)) return null;
|
|
284
|
+
const stat = lstatSync(filePath);
|
|
285
|
+
if (!stat.isFile()) {
|
|
286
|
+
return "__NON_FILE__";
|
|
287
|
+
}
|
|
288
|
+
return hashFile(filePath);
|
|
289
|
+
}
|
|
290
|
+
function commandExists(command2) {
|
|
291
|
+
try {
|
|
292
|
+
if (process.platform === "win32") {
|
|
293
|
+
execSync(`where ${command2}`, { stdio: "ignore" });
|
|
294
|
+
} else {
|
|
295
|
+
execSync(`command -v ${command2}`, { stdio: "ignore", shell: "/bin/zsh" });
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
58
302
|
function findFreePort(startPort) {
|
|
59
|
-
return new Promise((
|
|
303
|
+
return new Promise((resolvePort, rejectPort) => {
|
|
60
304
|
const server = createServer();
|
|
61
305
|
server.listen(startPort, () => {
|
|
62
|
-
server.close(() =>
|
|
306
|
+
server.close(() => resolvePort(startPort));
|
|
63
307
|
});
|
|
64
308
|
server.on("error", (err) => {
|
|
65
309
|
if (err.code === "EADDRINUSE") {
|
|
66
|
-
|
|
310
|
+
resolvePort(findFreePort(startPort + 1));
|
|
67
311
|
} else {
|
|
68
|
-
|
|
312
|
+
rejectPort(err);
|
|
69
313
|
}
|
|
70
314
|
});
|
|
71
315
|
});
|
|
72
316
|
}
|
|
73
|
-
function
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
317
|
+
function getPackageRoot() {
|
|
318
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
319
|
+
}
|
|
320
|
+
function getPackageVersion(packageRoot2) {
|
|
321
|
+
const packageJson = readJson(join(packageRoot2, "package.json"));
|
|
322
|
+
return packageJson.version;
|
|
323
|
+
}
|
|
324
|
+
function getLocalBundleRoot(packageRoot2) {
|
|
325
|
+
const bundleRoot = join(packageRoot2, "bundle");
|
|
326
|
+
if (!existsSync(join(bundleRoot, "scaffold")) || !existsSync(join(bundleRoot, "toolchain"))) {
|
|
327
|
+
throw new Error("CLI bundle assets are missing. Run `npm run build` before using the local package.");
|
|
328
|
+
}
|
|
329
|
+
return bundleRoot;
|
|
330
|
+
}
|
|
331
|
+
function getScaffoldManifest(bundleRoot) {
|
|
332
|
+
return readJson(join(bundleRoot, "meta", "scaffold-manifest.json"));
|
|
333
|
+
}
|
|
334
|
+
function getToolchainVersion(bundleRoot) {
|
|
335
|
+
return readFileSync(join(bundleRoot, "toolchain", "VERSION"), "utf8").trim();
|
|
336
|
+
}
|
|
337
|
+
function getManagedFilesPath(projectRoot) {
|
|
338
|
+
return join(projectRoot, METADATA_DIRNAME, MANAGED_FILES_FILE);
|
|
339
|
+
}
|
|
340
|
+
function getProjectMetadataPath(projectRoot) {
|
|
341
|
+
return join(projectRoot, METADATA_DIRNAME, PROJECT_METADATA_FILE);
|
|
342
|
+
}
|
|
343
|
+
function writeProjectState(projectRoot, showpaneVersion, scaffoldManifest, toolchainVersion) {
|
|
344
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
345
|
+
const projectMetadata = {
|
|
346
|
+
schemaVersion: 1,
|
|
347
|
+
showpaneVersion,
|
|
348
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
349
|
+
toolchainVersion,
|
|
350
|
+
projectRoot,
|
|
351
|
+
installedAt: timestamp,
|
|
352
|
+
lastUpgradedAt: timestamp
|
|
353
|
+
};
|
|
354
|
+
writeJson(getProjectMetadataPath(projectRoot), projectMetadata);
|
|
355
|
+
writeJson(getManagedFilesPath(projectRoot), {
|
|
356
|
+
schemaVersion: 1,
|
|
357
|
+
showpaneVersion,
|
|
358
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
359
|
+
files: scaffoldManifest.files
|
|
78
360
|
});
|
|
79
361
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
362
|
+
function readManagedFiles(projectRoot) {
|
|
363
|
+
return readJson(
|
|
364
|
+
getManagedFilesPath(projectRoot)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
function readProjectMetadata(projectRoot) {
|
|
368
|
+
return readJson(getProjectMetadataPath(projectRoot));
|
|
369
|
+
}
|
|
370
|
+
function writeUpdatedProjectState(projectRoot, previousMetadata, showpaneVersion, scaffoldManifest, toolchainVersion) {
|
|
371
|
+
writeJson(getProjectMetadataPath(projectRoot), {
|
|
372
|
+
...previousMetadata,
|
|
373
|
+
showpaneVersion,
|
|
374
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
375
|
+
toolchainVersion,
|
|
376
|
+
lastUpgradedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
377
|
+
});
|
|
378
|
+
writeJson(getManagedFilesPath(projectRoot), {
|
|
379
|
+
schemaVersion: 1,
|
|
380
|
+
showpaneVersion,
|
|
381
|
+
scaffoldVersion: scaffoldManifest.scaffoldVersion,
|
|
382
|
+
files: scaffoldManifest.files
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function detectProjectRoot(explicitProjectPath) {
|
|
386
|
+
const candidatePaths = [
|
|
387
|
+
explicitProjectPath ? resolve(explicitProjectPath) : null,
|
|
388
|
+
resolve(process.cwd()),
|
|
389
|
+
resolve(process.cwd(), "app")
|
|
390
|
+
].filter(Boolean);
|
|
391
|
+
const configPath = join(SHOWPANE_HOME, "config.json");
|
|
392
|
+
if (existsSync(configPath)) {
|
|
393
|
+
const config = readJson(configPath);
|
|
394
|
+
if (config.app_path) {
|
|
395
|
+
candidatePaths.push(resolve(config.app_path));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const candidate of candidatePaths) {
|
|
399
|
+
if (existsSync(join(candidate, "package.json")) && existsSync(join(candidate, "prisma", "schema.prisma")) && existsSync(getProjectMetadataPath(candidate)) && existsSync(getManagedFilesPath(candidate))) {
|
|
400
|
+
return candidate;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw new Error("Could not find a Showpane project root. Run the command from the project directory or pass --project <path>.");
|
|
404
|
+
}
|
|
405
|
+
function parseEnvFile(filePath) {
|
|
406
|
+
const env = {};
|
|
407
|
+
if (!existsSync(filePath)) return env;
|
|
408
|
+
for (const rawLine of readFileSync(filePath, "utf8").split(/\r?\n/)) {
|
|
409
|
+
const line = rawLine.trim();
|
|
410
|
+
if (!line || line.startsWith("#")) continue;
|
|
411
|
+
const separatorIndex = line.indexOf("=");
|
|
412
|
+
if (separatorIndex === -1) continue;
|
|
413
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
414
|
+
let value = line.slice(separatorIndex + 1).trim();
|
|
415
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
416
|
+
value = value.slice(1, -1);
|
|
417
|
+
}
|
|
418
|
+
env[key] = value;
|
|
419
|
+
}
|
|
420
|
+
return env;
|
|
421
|
+
}
|
|
422
|
+
function cleanupEmptyParents(projectRoot, relativePath) {
|
|
423
|
+
let currentDir = dirname(join(projectRoot, relativePath));
|
|
424
|
+
while (currentDir.startsWith(projectRoot) && currentDir !== projectRoot) {
|
|
425
|
+
if (readdirSync(currentDir).length > 0) break;
|
|
426
|
+
rmSync(currentDir, { recursive: true, force: true });
|
|
427
|
+
currentDir = dirname(currentDir);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function buildUpgradePlan(projectRoot, currentManifest, targetManifest) {
|
|
431
|
+
const plan = {
|
|
432
|
+
additions: [],
|
|
433
|
+
updates: [],
|
|
434
|
+
deletions: [],
|
|
435
|
+
conflicts: []
|
|
436
|
+
};
|
|
437
|
+
const allPaths = /* @__PURE__ */ new Set([
|
|
438
|
+
...Object.keys(currentManifest),
|
|
439
|
+
...Object.keys(targetManifest)
|
|
440
|
+
]);
|
|
441
|
+
for (const relativePath of [...allPaths].sort()) {
|
|
442
|
+
const currentRecordedHash = currentManifest[relativePath];
|
|
443
|
+
const targetHash = targetManifest[relativePath];
|
|
444
|
+
const absolutePath = join(projectRoot, relativePath);
|
|
445
|
+
const currentHash = getPathSignature(absolutePath);
|
|
446
|
+
const existsNow = currentHash !== null;
|
|
447
|
+
const locallyModifiedManagedFile = currentRecordedHash !== void 0 && currentHash !== currentRecordedHash;
|
|
448
|
+
const collidingUnmanagedFile = currentRecordedHash === void 0 && targetHash !== void 0 && existsNow && currentHash !== targetHash;
|
|
449
|
+
if (locallyModifiedManagedFile || collidingUnmanagedFile) {
|
|
450
|
+
plan.conflicts.push(relativePath);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (targetHash === void 0) {
|
|
454
|
+
if (existsNow) {
|
|
455
|
+
plan.deletions.push(relativePath);
|
|
456
|
+
}
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!existsNow) {
|
|
460
|
+
plan.additions.push(relativePath);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (currentHash !== targetHash) {
|
|
464
|
+
plan.updates.push(relativePath);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return plan;
|
|
468
|
+
}
|
|
469
|
+
function applyUpgradePlan(projectRoot, scaffoldSource, plan) {
|
|
470
|
+
for (const relativePath of plan.deletions) {
|
|
471
|
+
removePath(join(projectRoot, relativePath));
|
|
472
|
+
cleanupEmptyParents(projectRoot, relativePath);
|
|
473
|
+
}
|
|
474
|
+
for (const relativePath of [...plan.additions, ...plan.updates]) {
|
|
475
|
+
const sourcePath = getBundledScaffoldPath(scaffoldSource, relativePath);
|
|
476
|
+
const targetPath = join(projectRoot, relativePath);
|
|
477
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
478
|
+
cpSync(sourcePath, targetPath);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function installDependencies(projectRoot, verbose) {
|
|
482
|
+
if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
483
|
+
if (verbose === void 0) {
|
|
484
|
+
run("npm ci", projectRoot, getInstallerEnv());
|
|
485
|
+
} else {
|
|
486
|
+
runInstallerCommand("npm ci", projectRoot, getInstallerEnv(), verbose);
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
if (verbose === void 0) {
|
|
490
|
+
run("npm install", projectRoot, getInstallerEnv());
|
|
491
|
+
} else {
|
|
492
|
+
runInstallerCommand("npm install", projectRoot, getInstallerEnv(), verbose);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function generateLocalDatabase(projectRoot, databaseUrl, verbose) {
|
|
497
|
+
const env = getInstallerEnv({
|
|
498
|
+
DATABASE_URL: databaseUrl
|
|
499
|
+
});
|
|
500
|
+
if (verbose === void 0) {
|
|
501
|
+
run("npm run prisma:db-push", projectRoot, env);
|
|
502
|
+
} else {
|
|
503
|
+
runInstallerCommand("npm run prisma:db-push", projectRoot, env, verbose);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function seedProject(projectRoot, databaseUrl, verbose) {
|
|
507
|
+
const env = getInstallerEnv({
|
|
508
|
+
DATABASE_URL: databaseUrl
|
|
509
|
+
});
|
|
510
|
+
if (verbose === void 0) {
|
|
511
|
+
run("npx tsx prisma/seed.ts", projectRoot, env);
|
|
512
|
+
} else {
|
|
513
|
+
runInstallerCommand("npx tsx prisma/seed.ts", projectRoot, env, verbose);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function maybeRunPostUpgradeSteps(projectRoot, changedPaths) {
|
|
517
|
+
const dependenciesChanged = changedPaths.some(
|
|
518
|
+
(relativePath) => ["package.json", "package-lock.json"].includes(relativePath)
|
|
519
|
+
);
|
|
520
|
+
const prismaChanged = changedPaths.some((relativePath) => relativePath.startsWith("prisma/"));
|
|
521
|
+
if (dependenciesChanged) {
|
|
522
|
+
blue("Refreshing project dependencies");
|
|
523
|
+
installDependencies(projectRoot);
|
|
524
|
+
}
|
|
525
|
+
if (prismaChanged) {
|
|
526
|
+
const env = parseEnvFile(join(projectRoot, ".env"));
|
|
527
|
+
if (env.DATABASE_URL?.startsWith("file:")) {
|
|
528
|
+
blue("Applying local SQLite schema updates");
|
|
529
|
+
run("npm run prisma:db-push", projectRoot, getInstallerEnv({
|
|
530
|
+
DATABASE_URL: env.DATABASE_URL
|
|
531
|
+
}));
|
|
532
|
+
} else {
|
|
533
|
+
blue("Refreshing Prisma client");
|
|
534
|
+
run("npm run prisma:generate", projectRoot, getInstallerEnv());
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function tryInitializeGitRepo(projectRoot, announce = true) {
|
|
539
|
+
if (!commandExists("git")) {
|
|
540
|
+
blue("Git not found; skipped repository initialization");
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
execSync("git init -q -b main", { cwd: projectRoot, stdio: "ignore" });
|
|
545
|
+
} catch {
|
|
546
|
+
execSync("git init -q", { cwd: projectRoot, stdio: "ignore" });
|
|
547
|
+
try {
|
|
548
|
+
execSync("git branch -M main", { cwd: projectRoot, stdio: "ignore" });
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
run("git add .", projectRoot);
|
|
554
|
+
execSync('git commit -m "Initial Showpane scaffold"', {
|
|
555
|
+
cwd: projectRoot,
|
|
556
|
+
stdio: "ignore",
|
|
557
|
+
env: {
|
|
558
|
+
...process.env,
|
|
559
|
+
GIT_AUTHOR_NAME: "Showpane",
|
|
560
|
+
GIT_AUTHOR_EMAIL: "showpane@local.invalid",
|
|
561
|
+
GIT_COMMITTER_NAME: "Showpane",
|
|
562
|
+
GIT_COMMITTER_EMAIL: "showpane@local.invalid"
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
if (announce) {
|
|
566
|
+
green("Git repository initialized");
|
|
567
|
+
}
|
|
568
|
+
} catch {
|
|
569
|
+
if (announce) {
|
|
570
|
+
blue("Initialized git repository without an initial commit");
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function installSharedSkillProjection(toolchainRoot) {
|
|
575
|
+
const sharedSource = join(toolchainRoot, "skills", "shared");
|
|
576
|
+
const sharedTarget = join(CLAUDE_SKILLS_DIR, SHOWPANE_SHARED_SKILL);
|
|
577
|
+
removePath(sharedTarget);
|
|
578
|
+
symlinkSync(
|
|
579
|
+
sharedSource,
|
|
580
|
+
sharedTarget,
|
|
581
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
function printCreateSuccessCard(projectRoot, projectName, url) {
|
|
585
|
+
console.log();
|
|
586
|
+
console.log(` ${GREEN}Showpane is ready${RESET}`);
|
|
587
|
+
console.log();
|
|
588
|
+
console.log(` ${BOLD}Project:${RESET} ${projectRoot}`);
|
|
589
|
+
console.log(` ${BOLD}App:${RESET} ${url}`);
|
|
590
|
+
console.log(` ${BOLD}Demo:${RESET} example / demo-only-password`);
|
|
591
|
+
console.log();
|
|
592
|
+
console.log(` ${BOLD}Next:${RESET}`);
|
|
593
|
+
console.log(` ${DIM}cd ${projectName} && claude${RESET}`);
|
|
594
|
+
console.log();
|
|
595
|
+
console.log(` ${BOLD}Try:${RESET}`);
|
|
596
|
+
console.log(` ${DIM}Create a portal for my call with Acme Health${RESET}`);
|
|
597
|
+
console.log();
|
|
598
|
+
}
|
|
599
|
+
function startDevServer(projectRoot, databaseUrl, noOpen, verbose) {
|
|
600
|
+
return new Promise(async (resolveStart, rejectStart) => {
|
|
601
|
+
const port = await findFreePort(3e3);
|
|
602
|
+
const url = `http://localhost:${port}`;
|
|
603
|
+
const devServer = spawn("npm", ["run", "dev"], {
|
|
604
|
+
cwd: projectRoot,
|
|
605
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
606
|
+
env: { ...process.env, PORT: String(port), DATABASE_URL: databaseUrl }
|
|
607
|
+
});
|
|
608
|
+
let ready = false;
|
|
609
|
+
let bufferedOutput = "";
|
|
610
|
+
const readyPattern = /ready in/i;
|
|
611
|
+
const handleChunk = (target) => (chunk) => {
|
|
612
|
+
const text = chunk.toString();
|
|
613
|
+
if (verbose) {
|
|
614
|
+
target.write(text);
|
|
615
|
+
} else if (ready) {
|
|
616
|
+
target.write(text);
|
|
617
|
+
} else {
|
|
618
|
+
bufferedOutput += text;
|
|
619
|
+
if (bufferedOutput.length > 1e5) {
|
|
620
|
+
bufferedOutput = bufferedOutput.slice(-1e5);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!ready && readyPattern.test(text)) {
|
|
624
|
+
ready = true;
|
|
625
|
+
stepSuccess("Start app");
|
|
626
|
+
if (!noOpen) {
|
|
627
|
+
blue(`Opening ${url}`);
|
|
628
|
+
try {
|
|
629
|
+
openBrowser(url);
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
resolveStart({ devServer, url });
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
devServer.stdout?.on("data", handleChunk(process.stdout));
|
|
637
|
+
devServer.stderr?.on("data", handleChunk(process.stderr));
|
|
638
|
+
devServer.on("error", (errorLike) => {
|
|
639
|
+
rejectStart(new StepCommandError(
|
|
640
|
+
errorLike instanceof Error ? errorLike.message : String(errorLike),
|
|
641
|
+
bufferedOutput.trim()
|
|
642
|
+
));
|
|
643
|
+
});
|
|
644
|
+
devServer.on("close", (code) => {
|
|
645
|
+
if (ready) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const message = code === null ? "Dev server exited before becoming ready." : `Dev server exited with code ${code} before becoming ready.`;
|
|
649
|
+
rejectStart(new StepCommandError(message, bufferedOutput.trim()));
|
|
650
|
+
});
|
|
651
|
+
});
|
|
84
652
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
653
|
+
function installSkillProjection(toolchainRoot) {
|
|
654
|
+
removePath(join(CLAUDE_SKILLS_DIR, "showpane"));
|
|
655
|
+
installSharedSkillProjection(toolchainRoot);
|
|
656
|
+
const installedSkills = [];
|
|
657
|
+
const skillsRoot = join(toolchainRoot, "skills");
|
|
658
|
+
const skillDirs = readdirSync(skillsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("portal-"));
|
|
659
|
+
for (const skillDir of skillDirs) {
|
|
660
|
+
const skillMdPath = join(skillsRoot, skillDir.name, "SKILL.md");
|
|
661
|
+
if (!existsSync(skillMdPath)) continue;
|
|
662
|
+
const skillNameMatch = readFileSync(skillMdPath, "utf8").match(/^name:\s*(.+)$/m);
|
|
663
|
+
const skillName = skillNameMatch?.[1]?.trim() || skillDir.name;
|
|
664
|
+
const targetDir = join(CLAUDE_SKILLS_DIR, skillName);
|
|
665
|
+
removePath(targetDir);
|
|
666
|
+
mkdirSync(targetDir, { recursive: true });
|
|
667
|
+
symlinkSync(skillMdPath, join(targetDir, "SKILL.md"));
|
|
668
|
+
installedSkills.push(skillName);
|
|
669
|
+
}
|
|
670
|
+
return installedSkills;
|
|
671
|
+
}
|
|
672
|
+
function syncToolchain(bundleRoot, showpaneVersion, announce = true) {
|
|
673
|
+
const sourceToolchain = join(bundleRoot, "toolchain");
|
|
674
|
+
const targetToolchain = join(TOOLCHAIN_DIR, showpaneVersion);
|
|
675
|
+
ensureDir(TOOLCHAIN_DIR);
|
|
676
|
+
ensureDir(SHOWPANE_HOME);
|
|
677
|
+
ensureDir(CLAUDE_SKILLS_DIR);
|
|
678
|
+
removePath(targetToolchain);
|
|
679
|
+
copyDirContents(sourceToolchain, targetToolchain);
|
|
680
|
+
const helperBinDir = join(SHOWPANE_HOME, "bin");
|
|
681
|
+
ensureDir(helperBinDir);
|
|
682
|
+
removePath(join(helperBinDir, "showpane-config"));
|
|
683
|
+
symlinkSync(
|
|
684
|
+
join(targetToolchain, "bin", "showpane-config"),
|
|
685
|
+
join(helperBinDir, "showpane-config")
|
|
686
|
+
);
|
|
687
|
+
removePath(CURRENT_TOOLCHAIN_LINK);
|
|
688
|
+
symlinkSync(
|
|
689
|
+
targetToolchain,
|
|
690
|
+
CURRENT_TOOLCHAIN_LINK,
|
|
691
|
+
process.platform === "win32" ? "junction" : "dir"
|
|
692
|
+
);
|
|
693
|
+
const installedSkills = installSkillProjection(targetToolchain);
|
|
694
|
+
if (announce) {
|
|
695
|
+
green(`Toolchain synced to v${showpaneVersion}`);
|
|
696
|
+
green(`${installedSkills.length} Claude Code skills installed`);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
installedSkills,
|
|
700
|
+
toolchainRoot: targetToolchain,
|
|
701
|
+
toolchainVersion: getToolchainVersion(bundleRoot)
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function extractBundleForVersion(version) {
|
|
705
|
+
const tempRoot = fs.mkdtempSync(join(os.tmpdir(), "showpane-upgrade-"));
|
|
706
|
+
const packResult = capture(`npm pack showpane@${version} --json`, tempRoot);
|
|
707
|
+
const packJson = JSON.parse(packResult);
|
|
708
|
+
const tarballName = packJson.at(-1)?.filename;
|
|
709
|
+
if (!tarballName) {
|
|
710
|
+
throw new Error(`Could not download showpane@${version}`);
|
|
711
|
+
}
|
|
712
|
+
run(
|
|
713
|
+
`tar -xzf ${JSON.stringify(join(tempRoot, tarballName))} -C ${JSON.stringify(tempRoot)}`,
|
|
714
|
+
tempRoot
|
|
715
|
+
);
|
|
716
|
+
return {
|
|
717
|
+
bundleRoot: join(tempRoot, "package", "bundle"),
|
|
718
|
+
cleanup() {
|
|
719
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function createProject(args) {
|
|
724
|
+
const packageRoot2 = getPackageRoot();
|
|
725
|
+
const bundleRoot = getLocalBundleRoot(packageRoot2);
|
|
726
|
+
const showpaneVersion = getPackageVersion(packageRoot2);
|
|
727
|
+
const scaffoldManifest = getScaffoldManifest(bundleRoot);
|
|
728
|
+
let options;
|
|
729
|
+
try {
|
|
730
|
+
options = parseCreateArgs(args);
|
|
731
|
+
} catch (errorLike) {
|
|
732
|
+
printBanner();
|
|
733
|
+
console.log();
|
|
734
|
+
error(errorLike instanceof Error ? errorLike.message : String(errorLike));
|
|
735
|
+
printCreateUsage();
|
|
736
|
+
process.exit(1);
|
|
91
737
|
}
|
|
92
738
|
printBanner();
|
|
93
|
-
const companyName = await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
739
|
+
const companyName = options.companyName ?? await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
94
740
|
if (!companyName) {
|
|
95
741
|
error("Company name is required.");
|
|
96
742
|
process.exit(1);
|
|
97
743
|
}
|
|
98
744
|
const slug = toSlug(companyName);
|
|
99
745
|
const dirName = `showpane-${slug}`;
|
|
746
|
+
const projectRoot = resolve(process.cwd(), dirName);
|
|
747
|
+
if (existsSync(projectRoot)) {
|
|
748
|
+
error(`Target directory already exists: ${dirName}/`);
|
|
749
|
+
process.exit(1);
|
|
750
|
+
}
|
|
100
751
|
console.log();
|
|
101
752
|
blue(`Setting up ${BOLD}${companyName}${RESET} portal as ${DIM}${dirName}/${RESET}`);
|
|
102
753
|
console.log();
|
|
754
|
+
stepStart("Create project");
|
|
103
755
|
try {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch {
|
|
109
|
-
error("Failed to clone repository. Check your internet connection and try again.");
|
|
110
|
-
process.exit(1);
|
|
756
|
+
copyScaffoldFiles(join(bundleRoot, "scaffold"), projectRoot, scaffoldManifest);
|
|
757
|
+
stepSuccess("Project created");
|
|
758
|
+
} catch (errorLike) {
|
|
759
|
+
stepFailure("Create project", errorLike);
|
|
111
760
|
}
|
|
112
|
-
|
|
761
|
+
stepStart("Install dependencies");
|
|
113
762
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
} catch {
|
|
117
|
-
|
|
118
|
-
|
|
763
|
+
installDependencies(projectRoot, options.verbose);
|
|
764
|
+
stepSuccess("Dependencies installed");
|
|
765
|
+
} catch (errorLike) {
|
|
766
|
+
stepFailure(
|
|
767
|
+
"Install dependencies",
|
|
768
|
+
errorLike,
|
|
769
|
+
"Check your Node.js version and network connection, then try again."
|
|
770
|
+
);
|
|
119
771
|
}
|
|
120
772
|
const authSecret = randomBytes(32).toString("hex");
|
|
121
773
|
const databaseUrl = "file:./dev.db";
|
|
122
|
-
|
|
774
|
+
writeFileSync(
|
|
775
|
+
join(projectRoot, ".env"),
|
|
776
|
+
`DATABASE_URL="${databaseUrl}"
|
|
123
777
|
AUTH_SECRET="${authSecret}"
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
778
|
+
`
|
|
779
|
+
);
|
|
780
|
+
stepStart("Configure database");
|
|
127
781
|
try {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
782
|
+
generateLocalDatabase(projectRoot, databaseUrl, options.verbose);
|
|
783
|
+
seedProject(projectRoot, databaseUrl, options.verbose);
|
|
784
|
+
stepSuccess("Database configured");
|
|
785
|
+
} catch (errorLike) {
|
|
786
|
+
stepFailure(
|
|
787
|
+
"Configure database",
|
|
788
|
+
errorLike,
|
|
789
|
+
"Check Prisma setup and the generated .env file, then retry the install."
|
|
790
|
+
);
|
|
135
791
|
}
|
|
792
|
+
stepStart("Install Claude skills");
|
|
793
|
+
let toolchainInfo;
|
|
136
794
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
795
|
+
toolchainInfo = syncToolchain(bundleRoot, showpaneVersion, false);
|
|
796
|
+
writeProjectState(
|
|
797
|
+
projectRoot,
|
|
798
|
+
showpaneVersion,
|
|
799
|
+
scaffoldManifest,
|
|
800
|
+
toolchainInfo.toolchainVersion
|
|
801
|
+
);
|
|
802
|
+
tryInitializeGitRepo(projectRoot, false);
|
|
803
|
+
stepSuccess("Claude skills installed");
|
|
804
|
+
} catch (errorLike) {
|
|
805
|
+
stepFailure(
|
|
806
|
+
"Install Claude skills",
|
|
807
|
+
errorLike,
|
|
808
|
+
"Check permissions for ~/.showpane and ~/.claude/skills, then try again."
|
|
809
|
+
);
|
|
143
810
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const oldSymlink = path.join(skillsTarget, "showpane");
|
|
811
|
+
stepStart("Start app");
|
|
812
|
+
let serverStart;
|
|
147
813
|
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
|
-
}
|
|
814
|
+
serverStart = await startDevServer(
|
|
815
|
+
projectRoot,
|
|
816
|
+
databaseUrl,
|
|
817
|
+
options.noOpen,
|
|
818
|
+
options.verbose
|
|
819
|
+
);
|
|
820
|
+
} catch (errorLike) {
|
|
821
|
+
stepFailure(
|
|
822
|
+
"Start app",
|
|
823
|
+
errorLike,
|
|
824
|
+
`Run ${BOLD}cd ${dirName} && npm run dev${RESET} for more detail.`
|
|
825
|
+
);
|
|
185
826
|
}
|
|
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) => {
|
|
827
|
+
printCreateSuccessCard(projectRoot, dirName, serverStart.url);
|
|
828
|
+
serverStart.devServer.on("close", (code) => {
|
|
212
829
|
if (code !== 0) {
|
|
213
830
|
error(`Dev server exited with code ${code}`);
|
|
214
831
|
}
|
|
215
832
|
process.exit(code ?? 1);
|
|
216
833
|
});
|
|
217
834
|
process.on("SIGINT", () => {
|
|
218
|
-
devServer.kill("SIGINT");
|
|
835
|
+
serverStart.devServer.kill("SIGINT");
|
|
219
836
|
});
|
|
220
837
|
process.on("SIGTERM", () => {
|
|
221
|
-
devServer.kill("SIGTERM");
|
|
838
|
+
serverStart.devServer.kill("SIGTERM");
|
|
222
839
|
});
|
|
223
840
|
}
|
|
224
|
-
|
|
841
|
+
async function syncCurrentToolchain() {
|
|
842
|
+
const packageRoot2 = getPackageRoot();
|
|
843
|
+
const bundleRoot = getLocalBundleRoot(packageRoot2);
|
|
844
|
+
const showpaneVersion = getPackageVersion(packageRoot2);
|
|
845
|
+
printBanner();
|
|
846
|
+
maybePrintShowpaneUpdateMessage(showpaneVersion);
|
|
847
|
+
console.log();
|
|
848
|
+
blue(`Syncing Showpane toolchain v${showpaneVersion}`);
|
|
849
|
+
console.log();
|
|
850
|
+
syncToolchain(bundleRoot, showpaneVersion);
|
|
851
|
+
}
|
|
852
|
+
function parseUpgradeArgs(args) {
|
|
853
|
+
const getArg = (flag) => {
|
|
854
|
+
const index = args.indexOf(flag);
|
|
855
|
+
return index !== -1 ? args[index + 1] : void 0;
|
|
856
|
+
};
|
|
857
|
+
return {
|
|
858
|
+
targetVersion: getArg("--to"),
|
|
859
|
+
projectPath: getArg("--project"),
|
|
860
|
+
dryRun: args.includes("--dry-run")
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
async function upgradeProject(args) {
|
|
864
|
+
const packageRoot2 = getPackageRoot();
|
|
865
|
+
const currentCliVersion = getPackageVersion(packageRoot2);
|
|
866
|
+
const { targetVersion, projectPath, dryRun } = parseUpgradeArgs(args);
|
|
867
|
+
const resolvedTargetVersion = targetVersion || currentCliVersion;
|
|
868
|
+
const projectRoot = detectProjectRoot(projectPath);
|
|
869
|
+
printBanner();
|
|
870
|
+
maybePrintShowpaneUpdateMessage(currentCliVersion);
|
|
871
|
+
console.log();
|
|
872
|
+
blue(`Preparing upgrade for ${DIM}${projectRoot}${RESET}`);
|
|
873
|
+
console.log();
|
|
874
|
+
const bundleSource = resolvedTargetVersion === currentCliVersion ? { bundleRoot: getLocalBundleRoot(packageRoot2), cleanup() {
|
|
875
|
+
} } : extractBundleForVersion(resolvedTargetVersion);
|
|
876
|
+
try {
|
|
877
|
+
const targetBundleRoot = bundleSource.bundleRoot;
|
|
878
|
+
const scaffoldManifest = getScaffoldManifest(targetBundleRoot);
|
|
879
|
+
const currentManagedFiles = readManagedFiles(projectRoot).files;
|
|
880
|
+
const projectMetadata = readProjectMetadata(projectRoot);
|
|
881
|
+
const plan = buildUpgradePlan(projectRoot, currentManagedFiles, scaffoldManifest.files);
|
|
882
|
+
if (plan.conflicts.length > 0) {
|
|
883
|
+
error(`Upgrade blocked by ${plan.conflicts.length} modified managed file(s).`);
|
|
884
|
+
for (const relativePath of plan.conflicts) {
|
|
885
|
+
console.error(` ${relativePath}`);
|
|
886
|
+
}
|
|
887
|
+
process.exit(1);
|
|
888
|
+
}
|
|
889
|
+
console.log(` Additions: ${plan.additions.length}`);
|
|
890
|
+
console.log(` Updates: ${plan.updates.length}`);
|
|
891
|
+
console.log(` Deletions: ${plan.deletions.length}`);
|
|
892
|
+
console.log(` Conflicts: ${plan.conflicts.length}`);
|
|
893
|
+
console.log();
|
|
894
|
+
if (dryRun) {
|
|
895
|
+
green(`Dry run complete for showpane@${resolvedTargetVersion}`);
|
|
896
|
+
process.exit(0);
|
|
897
|
+
}
|
|
898
|
+
applyUpgradePlan(projectRoot, join(targetBundleRoot, "scaffold"), plan);
|
|
899
|
+
maybeRunPostUpgradeSteps(projectRoot, [
|
|
900
|
+
...plan.additions,
|
|
901
|
+
...plan.updates,
|
|
902
|
+
...plan.deletions
|
|
903
|
+
]);
|
|
904
|
+
const toolchainInfo = syncToolchain(targetBundleRoot, resolvedTargetVersion);
|
|
905
|
+
writeUpdatedProjectState(
|
|
906
|
+
projectRoot,
|
|
907
|
+
projectMetadata,
|
|
908
|
+
resolvedTargetVersion,
|
|
909
|
+
scaffoldManifest,
|
|
910
|
+
toolchainInfo.toolchainVersion
|
|
911
|
+
);
|
|
912
|
+
green(`Project upgraded to showpane@${resolvedTargetVersion}`);
|
|
913
|
+
} finally {
|
|
914
|
+
bundleSource.cleanup();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
225
917
|
async function login() {
|
|
226
918
|
printBanner();
|
|
227
919
|
blue("Authenticating with Showpane...");
|
|
@@ -237,11 +929,11 @@ async function login() {
|
|
|
237
929
|
blue(`Opened ${verificationUrl}`);
|
|
238
930
|
console.log();
|
|
239
931
|
blue("Waiting for authorization...");
|
|
240
|
-
const
|
|
241
|
-
const
|
|
932
|
+
const pollInterval = 2e3;
|
|
933
|
+
const timeoutMs = 10 * 60 * 1e3;
|
|
242
934
|
const start = Date.now();
|
|
243
|
-
while (Date.now() - start <
|
|
244
|
-
await new Promise((
|
|
935
|
+
while (Date.now() - start < timeoutMs) {
|
|
936
|
+
await new Promise((resolveLater) => setTimeout(resolveLater, pollInterval));
|
|
245
937
|
const pollRes = await fetch(`${API_BASE}/api/cli/poll?code=${code}`);
|
|
246
938
|
if (pollRes.status === 410) {
|
|
247
939
|
error("Code expired. Please try again.");
|
|
@@ -251,43 +943,58 @@ async function login() {
|
|
|
251
943
|
throw new Error(`Poll failed (${pollRes.status})`);
|
|
252
944
|
}
|
|
253
945
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
946
|
+
if (data.status !== "approved") continue;
|
|
947
|
+
const configDir = join(homedir(), ".showpane");
|
|
948
|
+
mkdirSync(configDir, { recursive: true });
|
|
949
|
+
const configPath = join(configDir, "config.json");
|
|
950
|
+
writeFileSync(
|
|
951
|
+
configPath,
|
|
952
|
+
JSON.stringify(
|
|
953
|
+
{
|
|
954
|
+
accessToken: data.accessToken,
|
|
955
|
+
accessTokenExpiresAt: data.tokenExpiresAt,
|
|
956
|
+
orgSlug: data.orgSlug,
|
|
957
|
+
portalUrl: data.portalUrl,
|
|
958
|
+
vercelProjectId: data.vercelProjectId,
|
|
959
|
+
app_path: process.cwd(),
|
|
960
|
+
deploy_mode: "cloud"
|
|
961
|
+
},
|
|
962
|
+
null,
|
|
963
|
+
2
|
|
964
|
+
)
|
|
965
|
+
);
|
|
966
|
+
chmodSync(configPath, 384);
|
|
967
|
+
console.log();
|
|
968
|
+
green(`Authenticated! Connected to ${BOLD}${data.orgSlug}${RESET}`);
|
|
969
|
+
console.log();
|
|
970
|
+
return;
|
|
280
971
|
}
|
|
281
972
|
error("Authentication timed out. Please try again.");
|
|
282
973
|
process.exit(1);
|
|
283
974
|
}
|
|
284
|
-
|
|
975
|
+
var command = process.argv[2];
|
|
976
|
+
var packageRoot = getPackageRoot();
|
|
977
|
+
if (process.argv.includes("--version")) {
|
|
978
|
+
console.log(getPackageVersion(packageRoot));
|
|
979
|
+
process.exit(0);
|
|
980
|
+
}
|
|
981
|
+
if (command === "login") {
|
|
285
982
|
login().catch((err) => {
|
|
286
983
|
error(String(err));
|
|
287
984
|
process.exit(1);
|
|
288
985
|
});
|
|
986
|
+
} else if (command === "sync") {
|
|
987
|
+
syncCurrentToolchain().catch((err) => {
|
|
988
|
+
error(String(err));
|
|
989
|
+
process.exit(1);
|
|
990
|
+
});
|
|
991
|
+
} else if (command === "upgrade") {
|
|
992
|
+
upgradeProject(process.argv.slice(3)).catch((err) => {
|
|
993
|
+
error(String(err));
|
|
994
|
+
process.exit(1);
|
|
995
|
+
});
|
|
289
996
|
} else {
|
|
290
|
-
|
|
997
|
+
createProject(process.argv.slice(2)).catch((err) => {
|
|
291
998
|
error(String(err));
|
|
292
999
|
process.exit(1);
|
|
293
1000
|
});
|