veryfront 0.1.1228 → 0.1.1229

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 (126) hide show
  1. package/esm/cli/commands/build/command-help.js +1 -1
  2. package/esm/cli/commands/build/command.d.ts +12 -0
  3. package/esm/cli/commands/build/command.d.ts.map +1 -1
  4. package/esm/cli/commands/build/command.js +55 -5
  5. package/esm/cli/commands/demo/demo.d.ts.map +1 -1
  6. package/esm/cli/commands/demo/demo.js +12 -23
  7. package/esm/cli/commands/demo/dev-step.d.ts +29 -0
  8. package/esm/cli/commands/demo/dev-step.d.ts.map +1 -0
  9. package/esm/cli/commands/demo/dev-step.js +38 -0
  10. package/esm/cli/commands/dev/command.d.ts +46 -0
  11. package/esm/cli/commands/dev/command.d.ts.map +1 -1
  12. package/esm/cli/commands/dev/command.js +74 -25
  13. package/esm/cli/commands/dev/handler.d.ts.map +1 -1
  14. package/esm/cli/commands/dev/handler.js +5 -3
  15. package/esm/cli/commands/dev/port-fallback.d.ts +22 -0
  16. package/esm/cli/commands/dev/port-fallback.d.ts.map +1 -0
  17. package/esm/cli/commands/dev/port-fallback.js +89 -0
  18. package/esm/cli/commands/doctor/ai-checks.d.ts.map +1 -1
  19. package/esm/cli/commands/doctor/ai-checks.js +76 -19
  20. package/esm/cli/commands/doctor/index.d.ts.map +1 -1
  21. package/esm/cli/commands/doctor/index.js +12 -7
  22. package/esm/cli/commands/doctor/project-config.d.ts +10 -0
  23. package/esm/cli/commands/doctor/project-config.d.ts.map +1 -0
  24. package/esm/cli/commands/doctor/project-config.js +18 -0
  25. package/esm/cli/commands/doctor/project-structure.d.ts +7 -0
  26. package/esm/cli/commands/doctor/project-structure.d.ts.map +1 -1
  27. package/esm/cli/commands/doctor/project-structure.js +32 -14
  28. package/esm/cli/commands/doctor/server-checks.d.ts +8 -1
  29. package/esm/cli/commands/doctor/server-checks.d.ts.map +1 -1
  30. package/esm/cli/commands/doctor/server-checks.js +31 -6
  31. package/esm/cli/commands/init/config-generator.d.ts.map +1 -1
  32. package/esm/cli/commands/init/config-generator.js +14 -0
  33. package/esm/cli/commands/open/command-help.d.ts.map +1 -1
  34. package/esm/cli/commands/open/command-help.js +4 -1
  35. package/esm/cli/commands/open/handler.d.ts +13 -0
  36. package/esm/cli/commands/open/handler.d.ts.map +1 -1
  37. package/esm/cli/commands/open/handler.js +58 -11
  38. package/esm/cli/commands/routes/command.d.ts.map +1 -1
  39. package/esm/cli/commands/routes/command.js +4 -1
  40. package/esm/cli/commands/serve/command.d.ts +20 -0
  41. package/esm/cli/commands/serve/command.d.ts.map +1 -1
  42. package/esm/cli/commands/serve/command.js +35 -0
  43. package/esm/cli/commands/up/command.d.ts.map +1 -1
  44. package/esm/cli/commands/up/command.js +33 -4
  45. package/esm/cli/help/tips.js +1 -1
  46. package/esm/cli/scaffold/engine.d.ts.map +1 -1
  47. package/esm/cli/scaffold/engine.js +7 -3
  48. package/esm/cli/shared/project-creation.d.ts.map +1 -1
  49. package/esm/cli/shared/project-creation.js +7 -3
  50. package/esm/cli/templates/manifest.d.ts +6 -0
  51. package/esm/cli/templates/manifest.js +8 -2
  52. package/esm/cli/utils/env-prompt.js +1 -1
  53. package/esm/deno.js +1 -1
  54. package/esm/src/agent/composition/composition.d.ts.map +1 -1
  55. package/esm/src/agent/composition/composition.js +3 -2
  56. package/esm/src/agent/factory.d.ts.map +1 -1
  57. package/esm/src/agent/factory.js +14 -6
  58. package/esm/src/agent/hosted/cloud-agent-chat-execution.js +2 -2
  59. package/esm/src/agent/hosted/cloud-agent-child-tools.d.ts +3 -3
  60. package/esm/src/agent/hosted/cloud-agent-child-tools.d.ts.map +1 -1
  61. package/esm/src/agent/hosted/cloud-agent-child-tools.js +2 -2
  62. package/esm/src/agent/runtime/agent-delegation.d.ts +15 -0
  63. package/esm/src/agent/runtime/agent-delegation.d.ts.map +1 -1
  64. package/esm/src/agent/runtime/agent-delegation.js +55 -1
  65. package/esm/src/agent/runtime/index.d.ts.map +1 -1
  66. package/esm/src/agent/runtime/index.js +5 -2
  67. package/esm/src/agent/runtime/local-tool.d.ts +4 -0
  68. package/esm/src/agent/runtime/local-tool.d.ts.map +1 -1
  69. package/esm/src/agent/runtime/local-tool.js +15 -0
  70. package/esm/src/agent/runtime/mcp-server-tool-sources.js +2 -2
  71. package/esm/src/agent/runtime/skill-delegation-overrides.d.ts +1 -1
  72. package/esm/src/agent/runtime/skill-delegation-overrides.d.ts.map +1 -1
  73. package/esm/src/agent/runtime/skill-delegation-overrides.js +5 -2
  74. package/esm/src/build/production-build/build/build-setup.d.ts.map +1 -1
  75. package/esm/src/build/production-build/build/build-setup.js +6 -1
  76. package/esm/src/chat/index.d.ts +3 -1
  77. package/esm/src/chat/index.d.ts.map +1 -1
  78. package/esm/src/chat/index.js +3 -1
  79. package/esm/src/config/environment-config.d.ts.map +1 -1
  80. package/esm/src/config/environment-config.js +15 -4
  81. package/esm/src/discovery/import-rewriter.d.ts +8 -1
  82. package/esm/src/discovery/import-rewriter.d.ts.map +1 -1
  83. package/esm/src/discovery/import-rewriter.js +42 -2
  84. package/esm/src/errors/error-registry/config.d.ts +7 -0
  85. package/esm/src/errors/error-registry/config.d.ts.map +1 -1
  86. package/esm/src/errors/error-registry/config.js +14 -0
  87. package/esm/src/errors/error-registry/server.d.ts.map +1 -1
  88. package/esm/src/errors/error-registry/server.js +1 -1
  89. package/esm/src/errors/error-registry.d.ts +1 -0
  90. package/esm/src/errors/error-registry.d.ts.map +1 -1
  91. package/esm/src/errors/index.d.ts +1 -1
  92. package/esm/src/errors/index.d.ts.map +1 -1
  93. package/esm/src/errors/index.js +1 -1
  94. package/esm/src/extensions/first-party-import.js +4 -0
  95. package/esm/src/html/hydration-script-builder/hydration-runtime.generated.js +1 -1
  96. package/esm/src/html/styles-builder/tailwind-compiler.d.ts.map +1 -1
  97. package/esm/src/html/styles-builder/tailwind-compiler.js +17 -2
  98. package/esm/src/integrations/remote-tools.d.ts.map +1 -1
  99. package/esm/src/integrations/remote-tools.js +35 -6
  100. package/esm/src/markdown/index.d.ts +1 -1
  101. package/esm/src/markdown/index.js +1 -1
  102. package/esm/src/platform/compat/http/pinned-fetch.d.ts +1 -1
  103. package/esm/src/react/components/chat/chat/composition/chat-empty.d.ts +6 -1
  104. package/esm/src/react/components/chat/chat/composition/chat-empty.d.ts.map +1 -1
  105. package/esm/src/react/components/chat/chat/composition/chat-empty.js +6 -1
  106. package/esm/src/react/components/chat/chat/index.d.ts +1 -1
  107. package/esm/src/react/components/chat/chat/index.js +1 -1
  108. package/esm/src/react/components/chat/missing-renderer-warning.d.ts.map +1 -1
  109. package/esm/src/react/components/chat/missing-renderer-warning.js +1 -1
  110. package/esm/src/react/components/ui/alert.d.ts +2 -2
  111. package/esm/src/react/components/ui/alert.js +5 -5
  112. package/esm/src/react/components/ui/design-tokens.d.ts.map +1 -1
  113. package/esm/src/react/components/ui/design-tokens.js +4 -0
  114. package/esm/src/server/dev-server/server.d.ts +1 -0
  115. package/esm/src/server/dev-server/server.d.ts.map +1 -1
  116. package/esm/src/server/dev-server/server.js +50 -3
  117. package/esm/src/server/handlers/dev/framework-candidates.generated.d.ts.map +1 -1
  118. package/esm/src/server/handlers/dev/framework-candidates.generated.js +16 -3
  119. package/esm/src/transforms/mdx/esm-module-loader/cache/index.d.ts.map +1 -1
  120. package/esm/src/transforms/mdx/esm-module-loader/cache/index.js +4 -1
  121. package/esm/src/utils/cache-dir.d.ts +15 -0
  122. package/esm/src/utils/cache-dir.d.ts.map +1 -1
  123. package/esm/src/utils/cache-dir.js +61 -0
  124. package/esm/src/utils/version-constant.d.ts +1 -1
  125. package/esm/src/utils/version-constant.js +1 -1
  126. package/package.json +8 -8
@@ -6,7 +6,7 @@ export const buildHelp = {
6
6
  options: [
7
7
  {
8
8
  flag: "-o, --output <dir>",
9
- description: "Output directory",
9
+ description: "Output directory (also configurable via build.outDir)",
10
10
  default: "dist",
11
11
  },
12
12
  {
@@ -1,3 +1,4 @@
1
+ import { type VeryfrontConfig } from "../../../src/config/index.js";
1
2
  import type { BuildOptions } from "./types.js";
2
3
  /** @internal */
3
4
  export declare function runWithBundlerShutdown<T>(operation: () => Promise<T>, stopBundler?: () => Promise<void>): Promise<T>;
@@ -19,5 +20,16 @@ export declare function releaseBuildExtensions(loader: {
19
20
  teardownAll: () => Promise<void>;
20
21
  } | undefined): Promise<void>;
21
22
  export declare function formatBuildOutputPath(projectDir: string, outputDir: string): string;
23
+ /**
24
+ * Decide where the build writes.
25
+ *
26
+ * `-o/--output` wins, then `build.outDir` from veryfront.config.ts, then
27
+ * `dist`. `build.outDir` used to be read into the config object and dropped:
28
+ * the build wrote (and cleared) `dist` regardless, with no warning, so the
29
+ * documented way to keep the framework out of a project's own `dist/` did
30
+ * nothing. A relative `outDir` resolves against the project directory, which
31
+ * is how a config file naturally reads.
32
+ */
33
+ export declare function resolveBuildOutputDir(projectDir: string, explicitOutputDir: string | undefined, config: Pick<VeryfrontConfig, "build">): string;
22
34
  export declare function buildCommand(options: BuildOptions): Promise<void>;
23
35
  //# sourceMappingURL=command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/build/command.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,gBAAgB;AAChB,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,WAAW,GAAE,MAAM,OAAO,CAAC,IAAI,CAG9B,GACA,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE;IAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAAG,SAAS,GACvD,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAwGjE"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/build/command.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAQ/E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAK/C,gBAAgB;AAChB,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAC3B,WAAW,GAAE,MAAM,OAAO,CAAC,IAAI,CAG9B,GACA,OAAO,CAAC,CAAC,CAAC,CAiBZ;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE;IAAE,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,GAAG,SAAS,GACvD,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,MAAM,GAAG,SAAS,EACrC,MAAM,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,GACrC,MAAM,CAIR;AAyCD,wBAAgB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4GjE"}
@@ -1,6 +1,7 @@
1
- import { join, relative, resolve } from "../../../src/platform/compat/path/index.js";
1
+ import { isAbsolute, join, relative, resolve } from "../../../src/platform/compat/path/index.js";
2
2
  import { runtime } from "../../../src/platform/index.js";
3
3
  import { getConfig } from "../../../src/config/index.js";
4
+ import { CONFIG_INVALID } from "../../../src/errors/index.js";
4
5
  import { buildProduction } from "../../../src/build/index.js";
5
6
  import { withSpan } from "../../../src/observability/tracing/otlp-setup.js";
6
7
  import { cliLogger } from "../../utils/index.js";
@@ -62,9 +63,56 @@ export async function releaseBuildExtensions(loader) {
62
63
  export function formatBuildOutputPath(projectDir, outputDir) {
63
64
  return relative(projectDir, resolve(projectDir, outputDir)).replace(/\\/g, "/");
64
65
  }
66
+ /**
67
+ * Decide where the build writes.
68
+ *
69
+ * `-o/--output` wins, then `build.outDir` from veryfront.config.ts, then
70
+ * `dist`. `build.outDir` used to be read into the config object and dropped:
71
+ * the build wrote (and cleared) `dist` regardless, with no warning, so the
72
+ * documented way to keep the framework out of a project's own `dist/` did
73
+ * nothing. A relative `outDir` resolves against the project directory, which
74
+ * is how a config file naturally reads.
75
+ */
76
+ export function resolveBuildOutputDir(projectDir, explicitOutputDir, config) {
77
+ const outputDir = explicitOutputDir ?? resolveConfiguredOutputDir(projectDir, config);
78
+ assertOutputDirExcludesProject(projectDir, outputDir, explicitOutputDir !== undefined);
79
+ return outputDir;
80
+ }
81
+ function resolveConfiguredOutputDir(projectDir, config) {
82
+ const configured = config.build?.outDir;
83
+ if (configured === undefined || configured === "")
84
+ return join(projectDir, "dist");
85
+ return resolve(projectDir, configured);
86
+ }
87
+ /**
88
+ * Refuse an output directory that is the project directory or an ancestor of it.
89
+ *
90
+ * The build clears its output directory before writing, so an `outDir` of `.`
91
+ * or `..` would recursively delete the project's own source — or the workspace
92
+ * above it. That was unreachable while `build.outDir` was ignored; now that the
93
+ * config value is honored, a stale compatibility-era config could reach it.
94
+ * Failing loudly is the only safe answer: silently substituting `dist` would
95
+ * reintroduce the ignored-configuration bug this change exists to fix.
96
+ */
97
+ function assertOutputDirExcludesProject(projectDir, outputDir, fromFlag) {
98
+ const resolvedOutput = resolve(projectDir, outputDir);
99
+ const relativeToOutput = relative(resolvedOutput, resolve(projectDir));
100
+ const containsProject = relativeToOutput === "" ||
101
+ (!relativeToOutput.startsWith("..") && !isAbsolute(relativeToOutput));
102
+ if (!containsProject)
103
+ return;
104
+ const source = fromFlag ? "-o/--output" : "build.outDir";
105
+ throw CONFIG_INVALID.create({
106
+ detail: `${source} resolves to ${resolvedOutput}, which is the project directory or contains it. ` +
107
+ `The build clears its output directory before writing, so this would delete the project. ` +
108
+ `Point the build at a directory of its own, such as dist.`,
109
+ });
110
+ }
65
111
  export function buildCommand(options) {
66
112
  return withSpan("cli.command.build", async () => {
67
- const outputDir = options.outputDir ?? join(options.projectDir, "dist");
113
+ // Placeholder until the config is loaded; every path that reports or
114
+ // writes the output directory runs after resolveBuildOutputDir below.
115
+ let outputDir = options.outputDir ?? join(options.projectDir, "dist");
68
116
  const startTime = Date.now();
69
117
  const dryRun = options.dryRun ?? false;
70
118
  let extensions;
@@ -79,12 +127,14 @@ export function buildCommand(options) {
79
127
  if (isJsonMode()) {
80
128
  streamJsonLine({ type: "step", name: "config", status: "started" });
81
129
  }
82
- else {
83
- displayBuildConfig({ ...options, outputDir });
84
- }
85
130
  const stats = await runWithBundlerShutdown(async () => {
86
131
  const adapter = await runtime.get();
87
132
  const config = await getConfig(options.projectDir, adapter);
133
+ // Resolved from the loaded config, so the displayed, reported and
134
+ // written output directory are the same one.
135
+ outputDir = resolveBuildOutputDir(options.projectDir, options.outputDir, config);
136
+ if (!isJsonMode())
137
+ displayBuildConfig({ ...options, outputDir });
88
138
  // Compose the project's extensions before anything that resolves a
89
139
  // contract. Only server bootstrap used to do this, so the build ran
90
140
  // with whatever one-off shims had been added and failed on the rest.
@@ -1 +1 @@
1
- {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/demo/demo.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2DH,KAAK,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAsa9D,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8CAA8C;IAC9C,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AA+DD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmH1E"}
1
+ {"version":3,"file":"demo.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/demo/demo.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2DH,KAAK,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,WAAW,GAAG,OAAO,CAAC;AAsZ9D,MAAM,WAAW,WAAW;IAC1B,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8CAA8C;IAC9C,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AA+DD,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmH1E"}
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { chdir, cwd, promptSync, writeStdout } from "../../../src/platform/index.js";
7
7
  import { getStdinReader, setRawMode } from "../../../src/platform/index.js";
8
- import { AnimatedDotMatrix, bold, brand, BRAND_TRUECOLOR, dim, error, formatDuration, HIDE_CURSOR, muted, SHOW_CURSOR, success, successBanner, typeCommand, typeLine, } from "../../ui/index.js";
8
+ import { AnimatedDotMatrix, bold, brand, BRAND_TRUECOLOR, dim, error, formatDuration, HIDE_CURSOR, muted, SHOW_CURSOR, successBanner, typeCommand, typeLine, } from "../../ui/index.js";
9
9
  import { exitProcess, isTTY, logError } from "../../utils/index.js";
10
10
  import { createOAuthAuthorizationUrl, createOAuthState, readToken, saveToken, validateToken, } from "../../auth/index.js";
11
11
  import { canOpenBrowser, openBrowser } from "../../auth/browser.js";
@@ -21,6 +21,7 @@ import { resolveOrCreateProject } from "../../shared/project-resolution.js";
21
21
  import { getProjectTarget } from "../../shared/deployment-provenance.js";
22
22
  import { reserveProjectSlug } from "../../shared/reserve-slug.js";
23
23
  import { DEMO_STEPS } from "./steps.js";
24
+ import { runDemoDevStep } from "./dev-step.js";
24
25
  // ANSI escape codes
25
26
  const ESC = "\x1b";
26
27
  const CLEAR_SCREEN = `${ESC}[2J`;
@@ -336,29 +337,17 @@ async function executeStepAction(step, projectName, loginMethod) {
336
337
  console.log();
337
338
  console.log(` ${dim("Starting dev server...")}`);
338
339
  try {
339
- const result = await devCommand({
340
- port: 3000,
341
- projectDir,
342
- hmr: true,
343
- demoMode: true,
340
+ await runDemoDevStep({
341
+ start: () => devCommand({
342
+ port: 3000,
343
+ projectDir,
344
+ hmr: true,
345
+ demoMode: true,
346
+ }),
347
+ open: (url) => openBrowser(url),
348
+ waitForStop: () => waitForEnter(),
349
+ log: (line) => console.log(line),
344
350
  });
345
- await result.ready;
346
- console.log(` ${success("●")} ${brand("http://localhost:3000/")}`);
347
- console.log();
348
- console.log(` ${dim("Opening browser...")}`);
349
- try {
350
- await openBrowser("http://localhost:3000");
351
- }
352
- catch {
353
- // Ignore if browser can't be opened
354
- }
355
- console.log();
356
- console.log(` ${dim("Press Enter to stop the dev server and continue...")}`);
357
- await waitForEnter();
358
- console.log();
359
- console.log(` ${dim("Stopping dev server...")}`);
360
- await result.stop();
361
- await result.done;
362
351
  }
363
352
  catch (e) {
364
353
  console.log(` ${error("✗")} ${e instanceof Error ? e.message : String(e)}`);
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The demo's dev-server step.
3
+ *
4
+ * Lives apart from `demo.ts` so the address the demo shows and opens can be
5
+ * tested without booting a dev server, a browser, or stdin.
6
+ *
7
+ * @module cli/commands/demo/dev-step
8
+ */
9
+ import type { DevCommandResult } from "../dev/index.js";
10
+ export interface DemoDevStepDeps {
11
+ /** Starts the dev server. */
12
+ start: () => Promise<DevCommandResult>;
13
+ /** Opens the demo's browser at the given URL. */
14
+ open: (url: string) => Promise<unknown>;
15
+ /** Resolves when the viewer asks for the dev server to stop. */
16
+ waitForStop: () => Promise<unknown>;
17
+ /** Writes one line of demo output. */
18
+ log: (line: string) => void;
19
+ }
20
+ /**
21
+ * Shows the running dev server, opens it, then stops it on request.
22
+ *
23
+ * Both the printed URL and the opened URL come from `result.port` - the port
24
+ * the server actually bound. `veryfront dev` asks for 3000 but falls forward
25
+ * when that is taken, so keying off the requested port would send the viewer to
26
+ * whatever process caused the collision instead of to the demo.
27
+ */
28
+ export declare function runDemoDevStep(deps: DemoDevStepDeps): Promise<void>;
29
+ //# sourceMappingURL=dev-step.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-step.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/demo/dev-step.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvC,iDAAiD;IACjD,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,gEAAgE;IAChE,WAAW,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACpC,sCAAsC;IACtC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBzE"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The demo's dev-server step.
3
+ *
4
+ * Lives apart from `demo.ts` so the address the demo shows and opens can be
5
+ * tested without booting a dev server, a browser, or stdin.
6
+ *
7
+ * @module cli/commands/demo/dev-step
8
+ */
9
+ import { brand, dim, success } from "../../ui/index.js";
10
+ /**
11
+ * Shows the running dev server, opens it, then stops it on request.
12
+ *
13
+ * Both the printed URL and the opened URL come from `result.port` - the port
14
+ * the server actually bound. `veryfront dev` asks for 3000 but falls forward
15
+ * when that is taken, so keying off the requested port would send the viewer to
16
+ * whatever process caused the collision instead of to the demo.
17
+ */
18
+ export async function runDemoDevStep(deps) {
19
+ const result = await deps.start();
20
+ await result.ready;
21
+ const serverUrl = `http://localhost:${result.port}`;
22
+ deps.log(` ${success("●")} ${brand(`${serverUrl}/`)}`);
23
+ deps.log("");
24
+ deps.log(` ${dim("Opening browser...")}`);
25
+ try {
26
+ await deps.open(serverUrl);
27
+ }
28
+ catch {
29
+ // Ignore if browser can't be opened
30
+ }
31
+ deps.log("");
32
+ deps.log(` ${dim("Press Enter to stop the dev server and continue...")}`);
33
+ await deps.waitForStop();
34
+ deps.log("");
35
+ deps.log(` ${dim("Stopping dev server...")}`);
36
+ await result.stop();
37
+ await result.done;
38
+ }
@@ -11,11 +11,24 @@ export interface DevOptions {
11
11
  open?: boolean;
12
12
  /** Demo mode: don't exit process on shutdown, resolve done promise instead */
13
13
  demoMode?: boolean;
14
+ /**
15
+ * Clear the shared on-disk ESM caches before starting. Only honoured once the
16
+ * requested dev port is confirmed free, because the cache directory is shared
17
+ * with any dev server already serving this project.
18
+ */
19
+ clearLocalCaches?: boolean;
14
20
  }
15
21
  export type DevCommandOptions = DevOptions;
16
22
  export interface DevCommandResult {
17
23
  ready: Promise<void>;
18
24
  done: Promise<void>;
25
+ /**
26
+ * The port the server actually bound, which is not always the requested one:
27
+ * a taken port falls forward. Embedded callers must use this rather than the
28
+ * port they asked for, or they will point the user at the process that caused
29
+ * the collision.
30
+ */
31
+ port: number;
19
32
  /** Stop the dev server programmatically (for demo mode) */
20
33
  stop: () => Promise<void>;
21
34
  }
@@ -24,5 +37,38 @@ export declare function preloadDevAuth(apiToken?: string): Promise<{
24
37
  projects: RemoteProject[];
25
38
  }>;
26
39
  export declare function createSelectedProjectPushOptions(projectDir: string, project: RemoteProject): PushOptions;
40
+ /**
41
+ * Starts the dev server on the first free port at or after `requestedPort`.
42
+ *
43
+ * Port 3000 is the most contended port on a developer machine, and the docs
44
+ * tell readers to run a bare `veryfront dev`, so a taken port scans forward
45
+ * rather than killing the command. Everything downstream - the MCP port, the
46
+ * printed URL, the browser the demo opens - must key off the returned `port`
47
+ * rather than the requested one, or a fall-forward points the user at the
48
+ * process that caused the collision.
49
+ *
50
+ * Takes `start` as a callback so the whole scan costs one `DevServer.start()`:
51
+ * probing is a bare bind/release, and a failed `start()` has already registered
52
+ * watchers and reload subscriptions that only `stop()` releases.
53
+ */
54
+ export declare function startDevServerOnFreePort<T>(requestedPort: number, start: (port: number) => Promise<T>): Promise<{
55
+ server: T;
56
+ port: number;
57
+ }>;
58
+ /**
59
+ * Clears the shared on-disk ESM caches, but only if `requestedPort` is free.
60
+ *
61
+ * The caches live under the project's `.cache` directory, which every dev
62
+ * server rooted at that project shares. A taken dev port is the signal that one
63
+ * of them is already running and still serving modules it compiled, so the
64
+ * clear is skipped rather than wiping that server's work out from under it -
65
+ * the second `veryfront dev` falls forward to a free port and starts on a cache
66
+ * it did not just destroy.
67
+ *
68
+ * Returns whether the clear ran. Takes `clear` and `probe` as parameters so the
69
+ * decision can be tested without booting a dev server, the same seam
70
+ * `startDevServerOnFreePort` uses.
71
+ */
72
+ export declare function clearLocalCachesIfPortFree(requestedPort: number, clear?: () => Promise<void>, probe?: (port: number) => Promise<boolean>): Promise<boolean>;
27
73
  export declare function devCommand(options: DevOptions): Promise<DevCommandResult>;
28
74
  //# sourceMappingURL=command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/dev/command.ts"],"names":[],"mappings":"AAAA;;GAEG;AAkBH,OAAO,EAAE,KAAK,YAAY,EAA2B,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE9E,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAI1F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAQD,wBAAsB,cAAc,CAClC,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC,CAQvE;AAED,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,WAAW,CAEb;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAwRzE"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/dev/command.ts"],"names":[],"mappings":"AAAA;;GAEG;AAmBH,OAAO,EAAE,KAAK,YAAY,EAA2B,MAAM,qBAAqB,CAAC;AACjF,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAE9E,OAAO,EAAwC,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK1F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,MAAM,iBAAiB,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IACpB;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAQD,wBAAsB,cAAc,CAClC,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;IAAE,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC,CAQvE;AAED,wBAAgB,gCAAgC,CAC9C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,aAAa,GACrB,WAAW,CAEb;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,wBAAwB,CAAC,CAAC,EAC9C,aAAa,EAAE,MAAM,EACrB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC;IAAE,MAAM,EAAE,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBtC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,0BAA0B,CAC9C,aAAa,EAAE,MAAM,EACrB,KAAK,GAAE,MAAM,OAAO,CAAC,IAAI,CAAuB,EAChD,KAAK,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAmB,GAC1D,OAAO,CAAC,OAAO,CAAC,CAIlB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAyRzE"}
@@ -2,12 +2,13 @@
2
2
  * Dev Command - Development server with HMR
3
3
  */
4
4
  import { compileAllMDX, watchMDX } from "../../../src/build/index.js";
5
- import { CONFIG_NOT_FOUND, INITIALIZATION_ERROR } from "../../../src/errors/index.js";
5
+ import { CONFIG_NOT_FOUND, PORT_IN_USE } from "../../../src/errors/index.js";
6
6
  import { join } from "../../../src/platform/compat/path/index.js";
7
7
  import { runtime } from "../../../src/platform/index.js";
8
8
  import { getConfig } from "../../../src/config/index.js";
9
9
  import { getEnvironmentConfig } from "../../../src/config/index.js";
10
10
  import { startDevServer } from "../../../src/server/index.js";
11
+ import { clearAllLocalCaches } from "../../../src/transforms/mdx/esm-module-loader/cache/index.js";
11
12
  import { validateProviderConfig } from "../../../src/discovery/index.js";
12
13
  import { brand, devShortcuts, dim, error as errorColor, formatDuration, warning } from "../../ui/index.js";
13
14
  import { exitProcess, isTTY, isVerbose, registerTerminationSignals } from "../../utils/index.js";
@@ -23,6 +24,7 @@ import { pullCommand } from "../pull/index.js";
23
24
  import { createStagedPushOptions, pushCommand } from "../push/index.js";
24
25
  import { createProjectSelector } from "./project-selector.js";
25
26
  import { createDevLogController } from "./log-controller.js";
27
+ import { findAvailablePort, isPortAvailable, isPortInUseError } from "./port-fallback.js";
26
28
  function authStatus(identity) {
27
29
  return isApiKeyIdentity(identity)
28
30
  ? "Authenticated with an API key"
@@ -40,9 +42,64 @@ export async function preloadDevAuth(apiToken) {
40
42
  export function createSelectedProjectPushOptions(projectDir, project) {
41
43
  return createStagedPushOptions(project.slug, projectDir);
42
44
  }
45
+ /**
46
+ * Starts the dev server on the first free port at or after `requestedPort`.
47
+ *
48
+ * Port 3000 is the most contended port on a developer machine, and the docs
49
+ * tell readers to run a bare `veryfront dev`, so a taken port scans forward
50
+ * rather than killing the command. Everything downstream - the MCP port, the
51
+ * printed URL, the browser the demo opens - must key off the returned `port`
52
+ * rather than the requested one, or a fall-forward points the user at the
53
+ * process that caused the collision.
54
+ *
55
+ * Takes `start` as a callback so the whole scan costs one `DevServer.start()`:
56
+ * probing is a bare bind/release, and a failed `start()` has already registered
57
+ * watchers and reload subscriptions that only `stop()` releases.
58
+ */
59
+ export async function startDevServerOnFreePort(requestedPort, start) {
60
+ const port = await findAvailablePort(requestedPort);
61
+ if (port !== requestedPort) {
62
+ console.log();
63
+ console.log(` ${warning("!")} Port ${requestedPort} is in use, using ${port} instead`);
64
+ }
65
+ try {
66
+ return { server: await start(port), port };
67
+ }
68
+ catch (error) {
69
+ // Lost the race between probing the port and binding it.
70
+ if (isPortInUseError(error)) {
71
+ throw PORT_IN_USE.create({
72
+ detail: `Port ${port} is already in use`,
73
+ cause: error,
74
+ context: { port },
75
+ });
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+ /**
81
+ * Clears the shared on-disk ESM caches, but only if `requestedPort` is free.
82
+ *
83
+ * The caches live under the project's `.cache` directory, which every dev
84
+ * server rooted at that project shares. A taken dev port is the signal that one
85
+ * of them is already running and still serving modules it compiled, so the
86
+ * clear is skipped rather than wiping that server's work out from under it -
87
+ * the second `veryfront dev` falls forward to a free port and starts on a cache
88
+ * it did not just destroy.
89
+ *
90
+ * Returns whether the clear ran. Takes `clear` and `probe` as parameters so the
91
+ * decision can be tested without booting a dev server, the same seam
92
+ * `startDevServerOnFreePort` uses.
93
+ */
94
+ export async function clearLocalCachesIfPortFree(requestedPort, clear = clearAllLocalCaches, probe = isPortAvailable) {
95
+ if (!await probe(requestedPort))
96
+ return false;
97
+ await clear();
98
+ return true;
99
+ }
43
100
  export function devCommand(options) {
44
101
  return withSpan("cli.command.dev", async () => {
45
- const { port, projectDir, hmr = true, open = false, demoMode = false } = options;
102
+ const { port, projectDir, hmr = true, open = false, demoMode = false, clearLocalCaches = false, } = options;
46
103
  const startTime = Date.now();
47
104
  let doneResolve;
48
105
  const done = new Promise((resolve) => {
@@ -65,6 +122,8 @@ export function devCommand(options) {
65
122
  const DEFAULT_DEV_PORT = 3000;
66
123
  const finalPort = port !== DEFAULT_DEV_PORT ? port : (config?.dev?.port ?? port);
67
124
  const enableHMR = config?.dev?.hmr !== false && hmr;
125
+ if (clearLocalCaches)
126
+ await clearLocalCachesIfPortFree(finalPort);
68
127
  const env = getEnvironmentConfig();
69
128
  const isProxyMode = config?.fs?.veryfront?.proxyMode === true;
70
129
  const linkedProjectSlug = await resolveLinkedProjectSlug(projectDir, config?.projectSlug ?? config?.fs?.veryfront?.projectSlug ?? env.projectSlug);
@@ -101,29 +160,17 @@ export function devCommand(options) {
101
160
  let identity = null;
102
161
  let projects = [];
103
162
  let selectedProject = null;
104
- try {
105
- devServer = await startDevServer({
106
- port: finalPort,
107
- projectDir,
108
- enableHMR,
109
- enableFastRefresh: true,
110
- signal: shutdownController.signal,
111
- });
112
- }
113
- catch (error) {
114
- if (error instanceof Error) {
115
- const msg = error.message.toLowerCase();
116
- if (msg.includes("eaddrinuse") || msg.includes("address already in use")) {
117
- throw INITIALIZATION_ERROR.create({
118
- detail: `Port ${finalPort} is already in use`,
119
- context: { port: finalPort },
120
- });
121
- }
122
- }
123
- throw error;
124
- }
163
+ const started = await startDevServerOnFreePort(finalPort, (port) => startDevServer({
164
+ port,
165
+ projectDir,
166
+ enableHMR,
167
+ enableFastRefresh: true,
168
+ signal: shutdownController.signal,
169
+ }));
170
+ devServer = started.server;
171
+ const boundPort = started.port;
125
172
  const DEV_MCP_PORT_OFFSET = 2;
126
- const mcpPort = finalPort + DEV_MCP_PORT_OFFSET;
173
+ const mcpPort = boundPort + DEV_MCP_PORT_OFFSET;
127
174
  try {
128
175
  mcpServer = await createMCPServer({ httpPort: mcpPort });
129
176
  }
@@ -196,10 +243,11 @@ export function devCommand(options) {
196
243
  return {
197
244
  ready: devServer.ready,
198
245
  done,
246
+ port: boundPort,
199
247
  stop: shutdown,
200
248
  };
201
249
  }
202
- const serverUrl = `http://veryfront.me:${finalPort}`;
250
+ const serverUrl = `http://veryfront.me:${boundPort}`;
203
251
  const elapsed = Date.now() - startTime;
204
252
  console.log();
205
253
  console.log(` ✓ Ready in ${formatDuration(elapsed)}`);
@@ -275,6 +323,7 @@ export function devCommand(options) {
275
323
  return {
276
324
  ready: devServer.ready,
277
325
  done,
326
+ port: boundPort,
278
327
  stop: shutdown,
279
328
  };
280
329
  }, {
@@ -1 +1 @@
1
- {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/dev/handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAexD,eAAO,MAAM,YAAY;;;;;;;GAOvB,CAAC;AAwBH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BtE"}
1
+ {"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/dev/handler.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAexD,eAAO,MAAM,YAAY;;;;;;;GAOvB,CAAC;AAwBH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BtE"}
@@ -7,7 +7,6 @@ import { cwd, setEnv } from "../../../src/platform/index.js";
7
7
  import { createFileSystem } from "../../../src/platform/index.js";
8
8
  import { cliLogger, DEFAULT_DEV_SERVER_PORT, showHeader } from "../../utils/index.js";
9
9
  import { refreshLoggerConfig } from "../../../src/utils/index.js";
10
- import { clearAllLocalCaches } from "../../../src/transforms/mdx/esm-module-loader/cache/index.js";
11
10
  import { createArgParser, parseArgsOrThrow } from "../../shared/args.js";
12
11
  import { ensureCliBundlerContracts } from "../../shared/default-contracts.js";
13
12
  const getDevArgsSchema = defineSchema((v) => v.object({
@@ -54,14 +53,17 @@ export async function handleDevCommand(args) {
54
53
  setEnv("VERYFRONT_DEBUG", "1");
55
54
  refreshLoggerConfig();
56
55
  }
57
- // Clear stale ESM caches to prevent module resolution issues from previous runs
58
- await clearAllLocalCaches();
59
56
  const { devCommand } = await import("./index.js");
60
57
  const { done } = await devCommand({
61
58
  port: opts.port,
62
59
  projectDir,
63
60
  hmr: opts.hmr && !opts.noHmr,
64
61
  open: opts.open,
62
+ // Clear stale ESM caches to prevent module resolution issues from previous
63
+ // runs. devCommand does this only once it has resolved the dev port and
64
+ // found it free — the caches are shared with any dev server already running
65
+ // against this project.
66
+ clearLocalCaches: true,
65
67
  });
66
68
  // Block until the dev server shuts down.
67
69
  // Without this, main.ts reaches exitProcess(0) and terminates immediately.
@@ -0,0 +1,22 @@
1
+ /** How many consecutive ports to try before giving up. */
2
+ export declare const MAX_PORT_FALLBACK_ATTEMPTS = 10;
3
+ /** The highest port a TCP listener can bind. */
4
+ export declare const MAX_TCP_PORT = 65535;
5
+ /** True when `error` means "that port is taken", on either Deno or Node. */
6
+ export declare function isPortInUseError(error: unknown): boolean;
7
+ /** Binds `port` and releases it again, to see whether the dev server could have it. */
8
+ export declare function isPortAvailable(port: number): Promise<boolean>;
9
+ /**
10
+ * Returns `requestedPort`, or the first free port after it.
11
+ *
12
+ * The scan stops at `MAX_TCP_PORT`: a runtime rejects port 65536 as an invalid
13
+ * port rather than as an address in use, so scanning past the end of the range
14
+ * would let that raw error escape instead of the `PORT_IN_USE` below.
15
+ * `requestedPort` itself is always probed, so an out-of-range `--port` still
16
+ * surfaces the runtime's own complaint about the value the user passed.
17
+ *
18
+ * Throws `PORT_IN_USE` - whose suggestion names `--port` - once the whole scan
19
+ * range is taken.
20
+ */
21
+ export declare function findAvailablePort(requestedPort: number, maxAttempts?: number, probe?: (port: number) => Promise<boolean>): Promise<number>;
22
+ //# sourceMappingURL=port-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"port-fallback.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/dev/port-fallback.ts"],"names":[],"mappings":"AAoBA,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C,gDAAgD;AAChD,eAAO,MAAM,YAAY,QAAQ,CAAC;AAElC,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAQxD;AAED,uFAAuF;AACvF,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBpE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,iBAAiB,CACrC,aAAa,EAAE,MAAM,EACrB,WAAW,GAAE,MAAmC,EAChD,KAAK,GAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAmB,GAC1D,OAAO,CAAC,MAAM,CAAC,CAejB"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Dev server port selection.
3
+ *
4
+ * Port 3000 is the most contended port on a developer machine, and the
5
+ * getting-started docs tell readers to run a bare `veryfront dev`. Rather than
6
+ * hard-failing when that port is taken, scan forward for the first free one -
7
+ * the same fallback the login callback server already uses.
8
+ *
9
+ * The scan probes ports by binding and releasing them, rather than by retrying
10
+ * `startDevServer`: a failed `DevServer.start()` has already registered file
11
+ * watchers and reload subscriptions that only `stop()` releases, so retrying it
12
+ * would leak a watcher set per busy port.
13
+ */
14
+ import * as dntShim from "../../../_dnt.shims.js";
15
+ import { LOCALHOST } from "../../../src/config/index.js";
16
+ import { PORT_IN_USE } from "../../../src/errors/index.js";
17
+ import { isDeno } from "../../../src/platform/index.js";
18
+ /** How many consecutive ports to try before giving up. */
19
+ export const MAX_PORT_FALLBACK_ATTEMPTS = 10;
20
+ /** The highest port a TCP listener can bind. */
21
+ export const MAX_TCP_PORT = 65535;
22
+ /** True when `error` means "that port is taken", on either Deno or Node. */
23
+ export function isPortInUseError(error) {
24
+ if (!(error instanceof Error))
25
+ return false;
26
+ // Deno: name is "AddrInUse", message "Address already in use (os error 48)".
27
+ // Node: code is "EADDRINUSE".
28
+ const code = error.code ?? "";
29
+ const message = error.message.toLowerCase();
30
+ return error.name === "AddrInUse" || code === "EADDRINUSE" ||
31
+ message.includes("eaddrinuse") || message.includes("address already in use");
32
+ }
33
+ /** Binds `port` and releases it again, to see whether the dev server could have it. */
34
+ export async function isPortAvailable(port) {
35
+ if (isDeno) {
36
+ try {
37
+ // @ts-ignore - Deno global
38
+ dntShim.Deno.listen({ hostname: LOCALHOST.IPV4, port }).close();
39
+ return true;
40
+ }
41
+ catch (error) {
42
+ if (isPortInUseError(error))
43
+ return false;
44
+ throw error;
45
+ }
46
+ }
47
+ const net = await import("node:net");
48
+ return await new Promise((resolve, reject) => {
49
+ const server = net.createServer();
50
+ server.unref?.();
51
+ server.once("error", (error) => {
52
+ if (isPortInUseError(error))
53
+ resolve(false);
54
+ else
55
+ reject(error);
56
+ });
57
+ server.listen({ port, host: LOCALHOST.IPV4, exclusive: true }, () => {
58
+ server.close(() => resolve(true));
59
+ });
60
+ });
61
+ }
62
+ /**
63
+ * Returns `requestedPort`, or the first free port after it.
64
+ *
65
+ * The scan stops at `MAX_TCP_PORT`: a runtime rejects port 65536 as an invalid
66
+ * port rather than as an address in use, so scanning past the end of the range
67
+ * would let that raw error escape instead of the `PORT_IN_USE` below.
68
+ * `requestedPort` itself is always probed, so an out-of-range `--port` still
69
+ * surfaces the runtime's own complaint about the value the user passed.
70
+ *
71
+ * Throws `PORT_IN_USE` - whose suggestion names `--port` - once the whole scan
72
+ * range is taken.
73
+ */
74
+ export async function findAvailablePort(requestedPort, maxAttempts = MAX_PORT_FALLBACK_ATTEMPTS, probe = isPortAvailable) {
75
+ const attempts = Math.max(1, maxAttempts);
76
+ let lastPort = requestedPort;
77
+ for (let attempt = 0; attempt < attempts; attempt++) {
78
+ const port = requestedPort + attempt;
79
+ if (attempt > 0 && port > MAX_TCP_PORT)
80
+ break;
81
+ if (await probe(port))
82
+ return port;
83
+ lastPort = port;
84
+ }
85
+ throw PORT_IN_USE.create({
86
+ detail: `Ports ${requestedPort}-${lastPort} are all in use`,
87
+ context: { requestedPort, lastPort, attempts },
88
+ });
89
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ai-checks.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/doctor/ai-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAInD;;GAEG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAyDnF"}
1
+ {"version":3,"file":"ai-checks.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/doctor/ai-checks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAuDnD;;GAEG;AACH,wBAAsB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CA6EnF"}