showpane 0.4.1 → 0.4.3

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