showpane 0.4.5 → 0.4.6
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.
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-04-09T14:
|
|
4
|
-
"scaffoldVersion": "0.2.
|
|
3
|
+
"generatedAt": "2026-04-09T14:26:32.270Z",
|
|
4
|
+
"scaffoldVersion": "0.2.3",
|
|
5
5
|
"files": {
|
|
6
6
|
".env.example": "0dd692f1c7e6bcabdf5dbdfe9abb73797d79d8e90da150d6098b63ddc695dc29",
|
|
7
7
|
".gitignore": "998e5f43865ea56ac79a05acfd5d4b0d696f310bd5325a1ed458c3d40154d437",
|
|
8
|
-
"VERSION": "
|
|
8
|
+
"VERSION": "3ab94c04d24986f3af288ba1cda2c0bbddbc5a89dff097182805f54578e1ea75",
|
|
9
9
|
"docker-compose.yml": "420fd123da019c22f03662933537e24779b4c2c91f90c23abfec5965cd0f35ce",
|
|
10
10
|
"docker/Caddyfile": "d9c58086986795f5b3e42ff9b5942e60b8df946a1a0c40351381616c0b4d2bed",
|
|
11
11
|
"docker/Dockerfile": "340470e3735ea53b2c03003a13a91361652291add33c40a2bf13e6af2a8cb73a",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"src/app/api/health/route.ts": "78fff55707372ce0cd6e9e49ef4f049622bc43cc42916d3f83e0162409d678b1",
|
|
48
48
|
"src/app/globals.css": "28dcda76006d0e6af01b6dcf1a315dc5b5b6931c880fc53fd6565ff09d5dd13a",
|
|
49
49
|
"src/app/layout.tsx": "c17aabeb2b486f023e777230343ace6cc06840f641a10b9dd9f65e092018f82f",
|
|
50
|
-
"src/app/page.tsx": "
|
|
50
|
+
"src/app/page.tsx": "732ea54f313386b65bce1170785379b27bb26b5da26b833b1e50c3713b87be1a",
|
|
51
51
|
"src/components/copy-button.tsx": "2f3d1d8a6a0a570c8d78e19c3c15519c44af17b5d8893ae5a5f57db5ecce7077",
|
|
52
52
|
"src/components/portal-login.tsx": "8b0d91bb28674e1102fd2e5b5ddcc3a93755dd806fbd3d1b2dbea2646cffca5e",
|
|
53
53
|
"src/components/portal-shell.tsx": "a4e16e118ef93f79e71fb69e80f1fac6e6fff90f0fbdacdf8deb821a57656877",
|
package/bundle/scaffold/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.2.
|
|
1
|
+
0.2.3
|
|
@@ -4,6 +4,7 @@ import { prisma } from "@/lib/db";
|
|
|
4
4
|
import { getRuntimeState, isRuntimeSnapshotMode } from "@/lib/runtime-state";
|
|
5
5
|
import { ArrowUpRight, BookOpen, Command, MessageSquareQuote } from "lucide-react";
|
|
6
6
|
import Link from "next/link";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
8
|
import os from "node:os";
|
|
8
9
|
import path from "node:path";
|
|
9
10
|
|
|
@@ -16,7 +17,20 @@ const PROMPT_EXAMPLES = [
|
|
|
16
17
|
export default async function Home() {
|
|
17
18
|
let portalCount = 0;
|
|
18
19
|
const showpaneBinDir = path.join(os.homedir(), ".showpane", "bin");
|
|
19
|
-
const
|
|
20
|
+
const configPath = path.join(os.homedir(), ".showpane", "config.json");
|
|
21
|
+
const configShellPathConfigured = existsSync(configPath)
|
|
22
|
+
? (() => {
|
|
23
|
+
try {
|
|
24
|
+
const raw = readFileSync(configPath, "utf8");
|
|
25
|
+
return Boolean((JSON.parse(raw) as { shellPathConfigured?: boolean }).shellPathConfigured);
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
})()
|
|
30
|
+
: false;
|
|
31
|
+
const prefersCanonicalCommand =
|
|
32
|
+
configShellPathConfigured ||
|
|
33
|
+
(process.env.PATH ?? "").split(path.delimiter).includes(showpaneBinDir);
|
|
20
34
|
const primaryCommand = "showpane claude";
|
|
21
35
|
const fallbackCommand = "npx showpane claude";
|
|
22
36
|
try {
|
package/bundle/toolchain/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.1.
|
|
1
|
+
1.1.3 (requires app >= 0.2.3)
|
package/dist/index.js
CHANGED
|
@@ -243,35 +243,146 @@ function getResumeCommand() {
|
|
|
243
243
|
function getResumeHint() {
|
|
244
244
|
return isShowpaneShimOnPath() ? null : `Optional: add ${SHOWPANE_BIN_DIR} to your PATH to use ${BOLD}showpane${RESET} directly.`;
|
|
245
245
|
}
|
|
246
|
+
function shellPathExportLine() {
|
|
247
|
+
return `export PATH="$HOME/.showpane/bin:$PATH"`;
|
|
248
|
+
}
|
|
249
|
+
function detectShellProfile() {
|
|
250
|
+
if (process.platform === "win32") {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const shell = basename(process.env.SHELL || "");
|
|
254
|
+
if (shell === "zsh") {
|
|
255
|
+
return join(homedir(), ".zshrc");
|
|
256
|
+
}
|
|
257
|
+
if (shell === "bash") {
|
|
258
|
+
return process.platform === "darwin" ? join(homedir(), ".bash_profile") : join(homedir(), ".bashrc");
|
|
259
|
+
}
|
|
260
|
+
if (shell === "fish") {
|
|
261
|
+
return join(homedir(), ".config", "fish", "config.fish");
|
|
262
|
+
}
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
function ensureShellPathEntry(profilePath) {
|
|
266
|
+
ensureDir(dirname(profilePath));
|
|
267
|
+
const exportLine = shellPathExportLine();
|
|
268
|
+
const contents = existsSync(profilePath) ? readFileSync(profilePath, "utf8") : "";
|
|
269
|
+
if (contents.includes(exportLine) || contents.includes(SHOWPANE_BIN_DIR)) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
const prefix = contents.length > 0 && !contents.endsWith("\n") ? "\n" : "";
|
|
273
|
+
writeFileSync(profilePath, `${contents}${prefix}${exportLine}
|
|
274
|
+
`);
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
async function maybeConfigureShellPath(config, options) {
|
|
278
|
+
const configuredProfile = typeof config.shellPathConfiguredProfile === "string" ? config.shellPathConfiguredProfile : null;
|
|
279
|
+
if (isShowpaneShimOnPath()) {
|
|
280
|
+
config.shellPathConfigured = true;
|
|
281
|
+
config.shellPathPrompted = true;
|
|
282
|
+
return {
|
|
283
|
+
command: "showpane claude",
|
|
284
|
+
configured: true,
|
|
285
|
+
profilePath: configuredProfile
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
if (config.shellPathConfigured) {
|
|
289
|
+
return {
|
|
290
|
+
command: "showpane claude",
|
|
291
|
+
configured: true,
|
|
292
|
+
profilePath: configuredProfile
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (options.yes || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
296
|
+
config.shellPathPrompted = true;
|
|
297
|
+
config.shellPathConfigured = false;
|
|
298
|
+
return {
|
|
299
|
+
command: "npx showpane claude",
|
|
300
|
+
configured: false,
|
|
301
|
+
profilePath: configuredProfile
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const profilePath = detectShellProfile();
|
|
305
|
+
if (!profilePath) {
|
|
306
|
+
config.shellPathPrompted = true;
|
|
307
|
+
config.shellPathConfigured = false;
|
|
308
|
+
return {
|
|
309
|
+
command: "npx showpane claude",
|
|
310
|
+
configured: false,
|
|
311
|
+
profilePath: null
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const answer = await ask(
|
|
315
|
+
` ${BOLD}Add Showpane to your PATH so 'showpane' works in future terminals?${RESET} ${DIM}(recommended) [Y/n]${RESET} `
|
|
316
|
+
);
|
|
317
|
+
if (answer && !["y", "yes"].includes(answer.toLowerCase())) {
|
|
318
|
+
config.shellPathPrompted = true;
|
|
319
|
+
config.shellPathConfigured = false;
|
|
320
|
+
config.shellPathConfiguredProfile = profilePath;
|
|
321
|
+
return {
|
|
322
|
+
command: "npx showpane claude",
|
|
323
|
+
configured: false,
|
|
324
|
+
profilePath
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
ensureShellPathEntry(profilePath);
|
|
328
|
+
config.shellPathPrompted = true;
|
|
329
|
+
config.shellPathConfigured = true;
|
|
330
|
+
config.shellPathConfiguredProfile = profilePath;
|
|
331
|
+
console.log();
|
|
332
|
+
green(`Added Showpane to your PATH in ${DIM}${profilePath}${RESET}`);
|
|
333
|
+
return {
|
|
334
|
+
command: "showpane claude",
|
|
335
|
+
configured: true,
|
|
336
|
+
profilePath
|
|
337
|
+
};
|
|
338
|
+
}
|
|
246
339
|
function getCommandOutput(errorLike) {
|
|
247
340
|
const error2 = errorLike;
|
|
248
341
|
const stdout = typeof error2?.stdout === "string" ? error2.stdout : error2?.stdout?.toString() ?? "";
|
|
249
342
|
const stderr = typeof error2?.stderr === "string" ? error2.stderr : error2?.stderr?.toString() ?? "";
|
|
250
343
|
return [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
251
344
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
345
|
+
async function runQuietAsync(command2, cwd, env) {
|
|
346
|
+
const child = spawn(command2, {
|
|
347
|
+
cwd,
|
|
348
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
349
|
+
shell: true,
|
|
350
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
351
|
+
});
|
|
352
|
+
let output = "";
|
|
353
|
+
const appendOutput = (chunk) => {
|
|
354
|
+
output += chunk.toString();
|
|
355
|
+
if (output.length > 20 * 1024 * 1024) {
|
|
356
|
+
output = output.slice(-20 * 1024 * 1024);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
child.stdout?.on("data", appendOutput);
|
|
360
|
+
child.stderr?.on("data", appendOutput);
|
|
361
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
362
|
+
child.on("error", (errorLike) => {
|
|
363
|
+
rejectPromise(
|
|
364
|
+
new StepCommandError(
|
|
365
|
+
errorLike instanceof Error ? errorLike.message : String(errorLike),
|
|
366
|
+
output.trim()
|
|
367
|
+
)
|
|
368
|
+
);
|
|
260
369
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
370
|
+
child.on("close", (code, signal) => {
|
|
371
|
+
if (code === 0) {
|
|
372
|
+
resolvePromise();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
const reason = signal ? `Command terminated with signal ${signal}` : `Command exited with code ${code ?? "unknown"}`;
|
|
376
|
+
rejectPromise(new StepCommandError(reason, output.trim()));
|
|
377
|
+
});
|
|
378
|
+
});
|
|
268
379
|
}
|
|
269
|
-
function runInstallerCommand(command2, cwd, env, verbose) {
|
|
380
|
+
async function runInstallerCommand(command2, cwd, env, verbose) {
|
|
270
381
|
if (verbose) {
|
|
271
382
|
run(command2, cwd, env);
|
|
272
383
|
return;
|
|
273
384
|
}
|
|
274
|
-
|
|
385
|
+
await runQuietAsync(command2, cwd, env);
|
|
275
386
|
}
|
|
276
387
|
var activeSpinner = null;
|
|
277
388
|
function renderSpinner(label, frame, startedAt) {
|
|
@@ -691,39 +802,39 @@ function applyUpgradePlan(projectRoot, scaffoldSource, plan) {
|
|
|
691
802
|
cpSync(sourcePath, targetPath);
|
|
692
803
|
}
|
|
693
804
|
}
|
|
694
|
-
function installDependencies(projectRoot, verbose) {
|
|
805
|
+
async function installDependencies(projectRoot, verbose) {
|
|
695
806
|
if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
696
807
|
if (verbose === void 0) {
|
|
697
808
|
run("npm ci", projectRoot, getInstallerEnv());
|
|
698
809
|
} else {
|
|
699
|
-
runInstallerCommand("npm ci", projectRoot, getInstallerEnv(), verbose);
|
|
810
|
+
await runInstallerCommand("npm ci", projectRoot, getInstallerEnv(), verbose);
|
|
700
811
|
}
|
|
701
812
|
} else {
|
|
702
813
|
if (verbose === void 0) {
|
|
703
814
|
run("npm install", projectRoot, getInstallerEnv());
|
|
704
815
|
} else {
|
|
705
|
-
runInstallerCommand("npm install", projectRoot, getInstallerEnv(), verbose);
|
|
816
|
+
await runInstallerCommand("npm install", projectRoot, getInstallerEnv(), verbose);
|
|
706
817
|
}
|
|
707
818
|
}
|
|
708
819
|
}
|
|
709
|
-
function generateLocalDatabase(projectRoot, databaseUrl, verbose) {
|
|
820
|
+
async function generateLocalDatabase(projectRoot, databaseUrl, verbose) {
|
|
710
821
|
const env = getInstallerEnv({
|
|
711
822
|
DATABASE_URL: databaseUrl
|
|
712
823
|
});
|
|
713
824
|
if (verbose === void 0) {
|
|
714
825
|
run("npm run prisma:db-push", projectRoot, env);
|
|
715
826
|
} else {
|
|
716
|
-
runInstallerCommand("npm run prisma:db-push", projectRoot, env, verbose);
|
|
827
|
+
await runInstallerCommand("npm run prisma:db-push", projectRoot, env, verbose);
|
|
717
828
|
}
|
|
718
829
|
}
|
|
719
|
-
function seedProject(projectRoot, databaseUrl, verbose) {
|
|
830
|
+
async function seedProject(projectRoot, databaseUrl, verbose) {
|
|
720
831
|
const env = getInstallerEnv({
|
|
721
832
|
DATABASE_URL: databaseUrl
|
|
722
833
|
});
|
|
723
834
|
if (verbose === void 0) {
|
|
724
835
|
run("npx tsx prisma/seed.ts", projectRoot, env);
|
|
725
836
|
} else {
|
|
726
|
-
runInstallerCommand("npx tsx prisma/seed.ts", projectRoot, env, verbose);
|
|
837
|
+
await runInstallerCommand("npx tsx prisma/seed.ts", projectRoot, env, verbose);
|
|
727
838
|
}
|
|
728
839
|
}
|
|
729
840
|
function maybeRunPostUpgradeSteps(projectRoot, changedPaths) {
|
|
@@ -794,9 +905,8 @@ function installSharedSkillProjection(toolchainRoot) {
|
|
|
794
905
|
process.platform === "win32" ? "junction" : "dir"
|
|
795
906
|
);
|
|
796
907
|
}
|
|
797
|
-
function printCreateSuccessCard(projectRoot, url) {
|
|
798
|
-
const resumeCommand =
|
|
799
|
-
const resumeHint = getResumeHint();
|
|
908
|
+
function printCreateSuccessCard(projectRoot, url, pathSetup) {
|
|
909
|
+
const resumeCommand = pathSetup.command;
|
|
800
910
|
console.log();
|
|
801
911
|
console.log(` ${GREEN}Showpane is ready${RESET}`);
|
|
802
912
|
console.log();
|
|
@@ -807,13 +917,17 @@ function printCreateSuccessCard(projectRoot, url) {
|
|
|
807
917
|
console.log(` ${BOLD}Next (in a new terminal window):${RESET}`);
|
|
808
918
|
console.log(` ${DIM}${resumeCommand}${RESET}`);
|
|
809
919
|
console.log();
|
|
810
|
-
|
|
920
|
+
if (pathSetup.configured && pathSetup.profilePath) {
|
|
921
|
+
console.log(` ${DIM}Your current terminal is running the local app logs. Open a fresh terminal so ${BOLD}showpane${RESET}${DIM} is available from ${pathSetup.profilePath}.${RESET}`);
|
|
922
|
+
} else {
|
|
923
|
+
console.log(` ${DIM}Your current terminal is running the local app logs, so open a fresh terminal before you run that command.${RESET}`);
|
|
924
|
+
}
|
|
811
925
|
console.log();
|
|
812
926
|
console.log(` ${BOLD}Try:${RESET}`);
|
|
813
927
|
console.log(` ${DIM}Create a portal for my call with Acme Health${RESET}`);
|
|
814
|
-
if (
|
|
928
|
+
if (!pathSetup.configured) {
|
|
815
929
|
console.log();
|
|
816
|
-
console.log(` ${DIM}${
|
|
930
|
+
console.log(` ${DIM}${getResumeHint()}${RESET}`);
|
|
817
931
|
}
|
|
818
932
|
console.log();
|
|
819
933
|
}
|
|
@@ -1045,6 +1159,8 @@ async function createProject(args) {
|
|
|
1045
1159
|
}
|
|
1046
1160
|
printBanner();
|
|
1047
1161
|
ensureShowpaneShim();
|
|
1162
|
+
const config = readShowpaneConfig();
|
|
1163
|
+
const pathSetup = await maybeConfigureShellPath(config, options);
|
|
1048
1164
|
const companyName = options.companyName ?? await ask(` ${BOLD}What's your company name?${RESET} `);
|
|
1049
1165
|
if (!companyName) {
|
|
1050
1166
|
error("Company name is required.");
|
|
@@ -1060,20 +1176,20 @@ async function createProject(args) {
|
|
|
1060
1176
|
console.log();
|
|
1061
1177
|
blue(`Setting up ${BOLD}${companyName}${RESET} portal as ${DIM}${dirName}/${RESET}`);
|
|
1062
1178
|
console.log();
|
|
1063
|
-
stepStartForCreate("
|
|
1179
|
+
stepStartForCreate("Creating project", options);
|
|
1064
1180
|
try {
|
|
1065
1181
|
copyScaffoldFiles(join(bundleRoot, "scaffold"), projectRoot, scaffoldManifest);
|
|
1066
1182
|
stepSuccessForCreate("Project created");
|
|
1067
1183
|
} catch (errorLike) {
|
|
1068
|
-
stepFailureForCreate("
|
|
1184
|
+
stepFailureForCreate("Creating project", errorLike);
|
|
1069
1185
|
}
|
|
1070
|
-
stepStartForCreate("
|
|
1186
|
+
stepStartForCreate("Installing dependencies", options);
|
|
1071
1187
|
try {
|
|
1072
|
-
installDependencies(projectRoot, options.verbose);
|
|
1188
|
+
await installDependencies(projectRoot, options.verbose);
|
|
1073
1189
|
stepSuccessForCreate("Dependencies installed");
|
|
1074
1190
|
} catch (errorLike) {
|
|
1075
1191
|
stepFailureForCreate(
|
|
1076
|
-
"
|
|
1192
|
+
"Installing dependencies",
|
|
1077
1193
|
errorLike,
|
|
1078
1194
|
"Check your Node.js version and network connection, then try again."
|
|
1079
1195
|
);
|
|
@@ -1086,23 +1202,22 @@ async function createProject(args) {
|
|
|
1086
1202
|
AUTH_SECRET="${authSecret}"
|
|
1087
1203
|
`
|
|
1088
1204
|
);
|
|
1089
|
-
stepStartForCreate("
|
|
1205
|
+
stepStartForCreate("Configuring database", options);
|
|
1090
1206
|
try {
|
|
1091
|
-
generateLocalDatabase(projectRoot, databaseUrl, options.verbose);
|
|
1092
|
-
seedProject(projectRoot, databaseUrl, options.verbose);
|
|
1207
|
+
await generateLocalDatabase(projectRoot, databaseUrl, options.verbose);
|
|
1208
|
+
await seedProject(projectRoot, databaseUrl, options.verbose);
|
|
1093
1209
|
stepSuccessForCreate("Database configured");
|
|
1094
1210
|
} catch (errorLike) {
|
|
1095
1211
|
stepFailureForCreate(
|
|
1096
|
-
"
|
|
1212
|
+
"Configuring database",
|
|
1097
1213
|
errorLike,
|
|
1098
1214
|
"Check Prisma setup and the generated .env file, then retry the install."
|
|
1099
1215
|
);
|
|
1100
1216
|
}
|
|
1101
|
-
stepStartForCreate("
|
|
1217
|
+
stepStartForCreate("Installing Claude skills", options);
|
|
1102
1218
|
let toolchainInfo;
|
|
1103
1219
|
try {
|
|
1104
1220
|
toolchainInfo = syncToolchain(bundleRoot, showpaneVersion, false);
|
|
1105
|
-
const config = readShowpaneConfig();
|
|
1106
1221
|
updateWorkspaceFromConfig(config, projectRoot, {
|
|
1107
1222
|
name: dirName,
|
|
1108
1223
|
deployMode: "local",
|
|
@@ -1119,12 +1234,12 @@ AUTH_SECRET="${authSecret}"
|
|
|
1119
1234
|
stepSuccessForCreate("Claude skills installed");
|
|
1120
1235
|
} catch (errorLike) {
|
|
1121
1236
|
stepFailureForCreate(
|
|
1122
|
-
"
|
|
1237
|
+
"Installing Claude skills",
|
|
1123
1238
|
errorLike,
|
|
1124
1239
|
"Check permissions for ~/.showpane and ~/.claude/skills, then try again."
|
|
1125
1240
|
);
|
|
1126
1241
|
}
|
|
1127
|
-
stepStartForCreate("
|
|
1242
|
+
stepStartForCreate("Starting app", options);
|
|
1128
1243
|
let serverStart;
|
|
1129
1244
|
try {
|
|
1130
1245
|
serverStart = await startDevServer(
|
|
@@ -1135,12 +1250,12 @@ AUTH_SECRET="${authSecret}"
|
|
|
1135
1250
|
);
|
|
1136
1251
|
} catch (errorLike) {
|
|
1137
1252
|
stepFailureForCreate(
|
|
1138
|
-
"
|
|
1253
|
+
"Starting app",
|
|
1139
1254
|
errorLike,
|
|
1140
1255
|
`Run ${BOLD}cd ${dirName} && npm run dev${RESET} for more detail.`
|
|
1141
1256
|
);
|
|
1142
1257
|
}
|
|
1143
|
-
printCreateSuccessCard(projectRoot, serverStart.url);
|
|
1258
|
+
printCreateSuccessCard(projectRoot, serverStart.url, pathSetup);
|
|
1144
1259
|
serverStart.devServer.on("close", (code) => {
|
|
1145
1260
|
if (code !== 0) {
|
|
1146
1261
|
error(`Dev server exited with code ${code}`);
|