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.
Files changed (106) hide show
  1. package/README.md +14 -1
  2. package/bundle/meta/scaffold-manifest.json +73 -0
  3. package/bundle/scaffold/VERSION +1 -0
  4. package/bundle/scaffold/__dot__env.example +24 -0
  5. package/bundle/scaffold/__dot__gitignore +41 -0
  6. package/bundle/scaffold/docker/Caddyfile +3 -0
  7. package/bundle/scaffold/docker/Dockerfile +30 -0
  8. package/bundle/scaffold/docker-compose.yml +53 -0
  9. package/bundle/scaffold/next.config.ts +20 -0
  10. package/bundle/scaffold/package-lock.json +5843 -0
  11. package/bundle/scaffold/package.json +42 -0
  12. package/bundle/scaffold/postcss.config.js +6 -0
  13. package/bundle/scaffold/prisma/migrations/20260408000000_init/migration.sql +143 -0
  14. package/bundle/scaffold/prisma/migrations/20260408010000_add_visitor_tracking/migration.sql +6 -0
  15. package/bundle/scaffold/prisma/migrations/20260409040000_add_portal_file_checksum/migration.sql +2 -0
  16. package/bundle/scaffold/prisma/migrations/migration_lock.toml +3 -0
  17. package/bundle/scaffold/prisma/schema.local.prisma +131 -0
  18. package/bundle/scaffold/prisma/schema.prisma +128 -0
  19. package/bundle/scaffold/prisma/seed.ts +49 -0
  20. package/bundle/scaffold/public/example-avatar.svg +4 -0
  21. package/bundle/scaffold/public/example-logo.svg +4 -0
  22. package/bundle/scaffold/public/robots.txt +2 -0
  23. package/bundle/scaffold/scripts/backup.sh +19 -0
  24. package/bundle/scaffold/scripts/e2e-verify.sh +487 -0
  25. package/bundle/scaffold/scripts/prisma-db-push.mjs +7 -0
  26. package/bundle/scaffold/scripts/prisma-generate.mjs +3 -0
  27. package/bundle/scaffold/scripts/prisma-schema.mjs +74 -0
  28. package/bundle/scaffold/scripts/restore.sh +31 -0
  29. package/bundle/scaffold/src/__tests__/client-portals.test.ts +80 -0
  30. package/bundle/scaffold/src/__tests__/portal-contracts.test.ts +32 -0
  31. package/bundle/scaffold/src/app/(portal)/client/[slug]/page.tsx +79 -0
  32. package/bundle/scaffold/src/app/(portal)/client/[slug]/s/[token]/route.ts +22 -0
  33. package/bundle/scaffold/src/app/(portal)/client/example/example-client.tsx +372 -0
  34. package/bundle/scaffold/src/app/(portal)/client/example/page.tsx +5 -0
  35. package/bundle/scaffold/src/app/(portal)/client/layout.tsx +7 -0
  36. package/bundle/scaffold/src/app/(portal)/client/page.tsx +18 -0
  37. package/bundle/scaffold/src/app/api/client-auth/route.ts +82 -0
  38. package/bundle/scaffold/src/app/api/client-auth/share/route.ts +30 -0
  39. package/bundle/scaffold/src/app/api/client-events/route.ts +87 -0
  40. package/bundle/scaffold/src/app/api/client-files/[...path]/route.ts +80 -0
  41. package/bundle/scaffold/src/app/api/client-files/client-upload/route.ts +118 -0
  42. package/bundle/scaffold/src/app/api/client-files/route.ts +37 -0
  43. package/bundle/scaffold/src/app/api/client-files/upload/route.ts +131 -0
  44. package/bundle/scaffold/src/app/api/health/route.ts +19 -0
  45. package/bundle/scaffold/src/app/globals.css +7 -0
  46. package/bundle/scaffold/src/app/layout.tsx +25 -0
  47. package/bundle/scaffold/src/app/page.tsx +171 -0
  48. package/bundle/scaffold/src/components/portal-login.tsx +169 -0
  49. package/bundle/scaffold/src/components/portal-shell.tsx +373 -0
  50. package/bundle/scaffold/src/lib/abuse-controls.ts +43 -0
  51. package/bundle/scaffold/src/lib/branding.ts +50 -0
  52. package/bundle/scaffold/src/lib/client-auth.ts +98 -0
  53. package/bundle/scaffold/src/lib/client-portals.ts +134 -0
  54. package/bundle/scaffold/src/lib/control-plane.ts +100 -0
  55. package/bundle/scaffold/src/lib/db.ts +7 -0
  56. package/bundle/scaffold/src/lib/files.ts +124 -0
  57. package/bundle/scaffold/src/lib/load-app-env.ts +42 -0
  58. package/bundle/scaffold/src/lib/portal-contracts.ts +69 -0
  59. package/bundle/scaffold/src/lib/prisma-client.ts +5 -0
  60. package/bundle/scaffold/src/lib/runtime-state.ts +69 -0
  61. package/bundle/scaffold/src/lib/storage.ts +204 -0
  62. package/bundle/scaffold/src/lib/token.ts +186 -0
  63. package/bundle/scaffold/src/lib/utils.ts +6 -0
  64. package/bundle/scaffold/src/middleware.ts +61 -0
  65. package/bundle/scaffold/tailwind.config.ts +15 -0
  66. package/bundle/scaffold/tests/__dot__gitkeep +0 -0
  67. package/bundle/scaffold/tsconfig.json +23 -0
  68. package/bundle/scaffold/vitest.config.ts +13 -0
  69. package/bundle/toolchain/VERSION +1 -0
  70. package/bundle/toolchain/bin/check-slug.ts +59 -0
  71. package/bundle/toolchain/bin/create-deploy-bundle.ts +93 -0
  72. package/bundle/toolchain/bin/create-portal.ts +71 -0
  73. package/bundle/toolchain/bin/delete-portal.ts +48 -0
  74. package/bundle/toolchain/bin/export-file-manifest.ts +84 -0
  75. package/bundle/toolchain/bin/export-runtime-state.ts +90 -0
  76. package/bundle/toolchain/bin/generate-share-link.ts +68 -0
  77. package/bundle/toolchain/bin/list-portals.ts +53 -0
  78. package/bundle/toolchain/bin/materialize-file.ts +35 -0
  79. package/bundle/toolchain/bin/query-analytics.ts +88 -0
  80. package/bundle/toolchain/bin/rotate-credentials.ts +57 -0
  81. package/bundle/toolchain/bin/showpane-config +63 -0
  82. package/bundle/toolchain/bin/tsconfig.json +13 -0
  83. package/bundle/toolchain/skills/VERSION +1 -0
  84. package/bundle/toolchain/skills/portal-analytics/SKILL.md +263 -0
  85. package/bundle/toolchain/skills/portal-create/SKILL.md +341 -0
  86. package/bundle/toolchain/skills/portal-credentials/SKILL.md +274 -0
  87. package/bundle/toolchain/skills/portal-delete/SKILL.md +265 -0
  88. package/bundle/toolchain/skills/portal-deploy/SKILL.md +721 -0
  89. package/bundle/toolchain/skills/portal-dev/SKILL.md +301 -0
  90. package/bundle/toolchain/skills/portal-list/SKILL.md +253 -0
  91. package/bundle/toolchain/skills/portal-onboard/SKILL.md +277 -0
  92. package/bundle/toolchain/skills/portal-preview/SKILL.md +257 -0
  93. package/bundle/toolchain/skills/portal-setup/SKILL.md +309 -0
  94. package/bundle/toolchain/skills/portal-share/SKILL.md +234 -0
  95. package/bundle/toolchain/skills/portal-status/SKILL.md +268 -0
  96. package/bundle/toolchain/skills/portal-update/SKILL.md +348 -0
  97. package/bundle/toolchain/skills/portal-upgrade/SKILL.md +235 -0
  98. package/bundle/toolchain/skills/portal-verify/SKILL.md +265 -0
  99. package/bundle/toolchain/skills/shared/bin/check-portal-guard.sh +49 -0
  100. package/bundle/toolchain/skills/shared/platform-constraints.md +33 -0
  101. package/bundle/toolchain/skills/shared/preamble.md +137 -0
  102. package/bundle/toolchain/templates/consulting/consulting-client.tsx +205 -0
  103. package/bundle/toolchain/templates/onboarding/onboarding-client.tsx +237 -0
  104. package/bundle/toolchain/templates/sales-followup/sales-followup-client.tsx +283 -0
  105. package/dist/index.js +873 -166
  106. 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 path from "path";
8
+ import fs from "fs";
10
9
  import os from "os";
10
+ import path from "path";
11
11
  import { fileURLToPath } from "url";
12
- var { chmodSync, mkdirSync, readFileSync, writeFileSync } = fs;
13
- var { resolve, dirname, join } = path;
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
- function green(msg) {
23
- console.log(` ${GREEN}\u2713${RESET} ${msg}`);
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(msg) {
26
- console.log(` ${BLUE}\u2192${RESET} ${msg}`);
55
+ function blue(message) {
56
+ console.log(` ${BLUE}\u2192${RESET} ${message}`);
27
57
  }
28
- function error(msg) {
29
- console.error(` ${RED}\u2717${RESET} ${msg}`);
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} Client portals that close deals.${RESET}
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((resolve2) => {
81
+ return new Promise((resolveAnswer) => {
49
82
  rl.question(question, (answer) => {
50
83
  rl.close();
51
- resolve2(answer.trim());
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((resolve2, reject) => {
303
+ return new Promise((resolvePort, rejectPort) => {
60
304
  const server = createServer();
61
305
  server.listen(startPort, () => {
62
- server.close(() => resolve2(startPort));
306
+ server.close(() => resolvePort(startPort));
63
307
  });
64
308
  server.on("error", (err) => {
65
309
  if (err.code === "EADDRINUSE") {
66
- resolve2(findFreePort(startPort + 1));
310
+ resolvePort(findFreePort(startPort + 1));
67
311
  } else {
68
- reject(err);
312
+ rejectPort(err);
69
313
  }
70
314
  });
71
315
  });
72
316
  }
73
- function run(cmd, cwd, env) {
74
- execSync(cmd, {
75
- cwd,
76
- stdio: "inherit",
77
- env: env ? { ...process.env, ...env } : process.env
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 openBrowser(url) {
81
- const platform = process.platform;
82
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
83
- exec(`${cmd} ${url}`);
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
- async function main() {
86
- if (process.argv.includes("--version")) {
87
- const __dirname = dirname(fileURLToPath(import.meta.url));
88
- const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
89
- console.log(pkg.version);
90
- process.exit(0);
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
- run(
105
- `git clone --depth 1 https://github.com/twillcocks/showpane.git ${dirName}`
106
- );
107
- green("Cloned repository");
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
- const appDir = resolve(process.cwd(), dirName, "app");
761
+ stepStart("Install dependencies");
113
762
  try {
114
- run("npm install", appDir);
115
- green("Dependencies installed");
116
- } catch {
117
- error("Failed to install dependencies.");
118
- process.exit(1);
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
- const envContent = `DATABASE_URL="${databaseUrl}"
774
+ writeFileSync(
775
+ join(projectRoot, ".env"),
776
+ `DATABASE_URL="${databaseUrl}"
123
777
  AUTH_SECRET="${authSecret}"
124
- `;
125
- writeFileSync(resolve(appDir, ".env"), envContent);
126
- green("Environment configured");
778
+ `
779
+ );
780
+ stepStart("Configure database");
127
781
  try {
128
- run("npx prisma db push --schema prisma/schema.local.prisma", appDir, {
129
- DATABASE_URL: databaseUrl
130
- });
131
- green("Database ready");
132
- } catch {
133
- error("Failed to set up the database. Check Prisma schema and try again.");
134
- process.exit(1);
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
- run("npx tsx prisma/seed.ts", appDir, {
138
- DATABASE_URL: databaseUrl
139
- });
140
- green("Example portal seeded");
141
- } catch {
142
- blue("Skipped example portal (seed failed \u2014 not a problem)");
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
- const skillsSource = path.resolve(process.cwd(), dirName, "skills");
145
- const skillsTarget = path.join(os.homedir(), ".claude", "skills");
146
- const oldSymlink = path.join(skillsTarget, "showpane");
811
+ stepStart("Start app");
812
+ let serverStart;
147
813
  try {
148
- const stat = fs.lstatSync(oldSymlink);
149
- if (stat.isSymbolicLink()) fs.unlinkSync(oldSymlink);
150
- } catch {
151
- }
152
- fs.mkdirSync(skillsTarget, { recursive: true });
153
- const skillDirs = fs.readdirSync(skillsSource, { withFileTypes: true }).filter((d) => d.isDirectory() && d.name.startsWith("portal-"));
154
- const installedSkills = [];
155
- for (const dir of skillDirs) {
156
- const skillMdPath = path.join(skillsSource, dir.name, "SKILL.md");
157
- if (!fs.existsSync(skillMdPath)) continue;
158
- const skillContent = fs.readFileSync(skillMdPath, "utf-8");
159
- const nameMatch = skillContent.match(/^name:\s*(.+)$/m);
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
- green(`${installedSkills.length} Claude Code skills installed`);
187
- const port = await findFreePort(3e3);
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
- var API_BASE = "https://app.showpane.com";
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 POLL_INTERVAL = 2e3;
241
- const TIMEOUT = 10 * 60 * 1e3;
932
+ const pollInterval = 2e3;
933
+ const timeoutMs = 10 * 60 * 1e3;
242
934
  const start = Date.now();
243
- while (Date.now() - start < TIMEOUT) {
244
- await new Promise((r) => setTimeout(r, POLL_INTERVAL));
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 === "approved") {
255
- const configDir = join(homedir(), ".showpane");
256
- mkdirSync(configDir, { recursive: true });
257
- const configPath = join(configDir, "config.json");
258
- writeFileSync(
259
- configPath,
260
- JSON.stringify(
261
- {
262
- accessToken: data.accessToken,
263
- accessTokenExpiresAt: data.tokenExpiresAt,
264
- orgSlug: data.orgSlug,
265
- portalUrl: data.portalUrl,
266
- vercelProjectId: data.vercelProjectId,
267
- app_path: join(process.cwd(), "app"),
268
- deploy_mode: "cloud"
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;
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
- if (process.argv[2] === "login") {
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
- main().catch((err) => {
997
+ createProject(process.argv.slice(2)).catch((err) => {
291
998
  error(String(err));
292
999
  process.exit(1);
293
1000
  });