visual-remote 0.1.3 → 0.3.0

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.
@@ -1439,8 +1439,8 @@ var CodexAdapter = class {
1439
1439
  };
1440
1440
 
1441
1441
  // ../../packages/bridge-core/src/config/loader.ts
1442
- import { readFile, realpath } from "node:fs/promises";
1443
- import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
1442
+ import { readFile, realpath, stat as stat2 } from "node:fs/promises";
1443
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
1444
1444
  import { parse as parseYaml } from "yaml";
1445
1445
  import { ZodError } from "zod";
1446
1446
 
@@ -1627,11 +1627,42 @@ function isWithinRoot(root, candidate) {
1627
1627
  const pathFromRoot = relative(root, candidate);
1628
1628
  return pathFromRoot === "" || !pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== "..";
1629
1629
  }
1630
+ async function configExists(root) {
1631
+ try {
1632
+ await stat2(join(root, CONFIG_PATH));
1633
+ return true;
1634
+ } catch (error) {
1635
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
1636
+ return false;
1637
+ }
1638
+ throw error;
1639
+ }
1640
+ }
1641
+ async function discoverVisualDevConfigRoot(startDirectory, repositoryRoot) {
1642
+ const repoRoot = await realpath(repositoryRoot);
1643
+ let cursor = await realpath(startDirectory);
1644
+ if (!isWithinRoot(repoRoot, cursor)) {
1645
+ throw new VisualDevConfigError(`Project directory is outside Git worktree: ${cursor}`);
1646
+ }
1647
+ while (true) {
1648
+ if (await configExists(cursor)) return cursor;
1649
+ if (cursor === repoRoot) return repoRoot;
1650
+ const parent = dirname(cursor);
1651
+ if (parent === cursor || !isWithinRoot(repoRoot, parent)) return repoRoot;
1652
+ cursor = parent;
1653
+ }
1654
+ }
1630
1655
  async function loadVisualDevConfig(repositoryRoot, options = {}) {
1631
1656
  const repoRoot = await realpath(repositoryRoot);
1632
- const configPath = join(repoRoot, CONFIG_PATH);
1633
- const localConfigPath = join(repoRoot, LOCAL_CONFIG_PATH);
1634
- const projectId = basename(repoRoot);
1657
+ const configRoot = await realpath(options.configRoot ?? repoRoot);
1658
+ if (!isWithinRoot(repoRoot, configRoot)) {
1659
+ throw new VisualDevConfigError(
1660
+ `Config directory is outside Git worktree: ${configRoot}`
1661
+ );
1662
+ }
1663
+ const configPath = join(configRoot, CONFIG_PATH);
1664
+ const localConfigPath = join(configRoot, LOCAL_CONFIG_PATH);
1665
+ const projectId = basename(configRoot);
1635
1666
  const baseDocument = await readYamlMapping(configPath, options.requireConfig ?? false);
1636
1667
  const localDocument = await readYamlMapping(localConfigPath, false);
1637
1668
  const loadedFiles = [];
@@ -1658,7 +1689,7 @@ async function loadVisualDevConfig(repositoryRoot, options = {}) {
1658
1689
  filePath: localDocument === void 0 ? configPath : localConfigPath
1659
1690
  });
1660
1691
  }
1661
- const unresolvedWorkspace = isAbsolute(config.project.workspace) ? config.project.workspace : resolve(repoRoot, config.project.workspace);
1692
+ const unresolvedWorkspace = isAbsolute(config.project.workspace) ? config.project.workspace : resolve(configRoot, config.project.workspace);
1662
1693
  let workspaceRoot;
1663
1694
  try {
1664
1695
  workspaceRoot = await realpath(unresolvedWorkspace);
@@ -1677,6 +1708,7 @@ async function loadVisualDevConfig(repositoryRoot, options = {}) {
1677
1708
  return {
1678
1709
  config,
1679
1710
  repoRoot,
1711
+ configRoot,
1680
1712
  workspaceRoot,
1681
1713
  configPath,
1682
1714
  localConfigPath,
@@ -1964,7 +1996,7 @@ import {
1964
1996
  rm
1965
1997
  } from "node:fs/promises";
1966
1998
  import { tmpdir } from "node:os";
1967
- import { dirname, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
1999
+ import { dirname as dirname2, relative as relative3, resolve as resolve4, sep as sep3 } from "node:path";
1968
2000
  var SNAPSHOT_ENV = {
1969
2001
  GIT_AUTHOR_NAME: "Visual Bridge",
1970
2002
  GIT_AUTHOR_EMAIL: "visual-bridge@localhost",
@@ -2317,7 +2349,7 @@ var GitTransactionManager = class _GitTransactionManager {
2317
2349
  } catch (error) {
2318
2350
  if (error.code !== "ENOENT") throw error;
2319
2351
  }
2320
- await this.#removeEmptyParents(dirname(absolute));
2352
+ await this.#removeEmptyParents(dirname2(absolute));
2321
2353
  }
2322
2354
  async #removeEmptyParents(start) {
2323
2355
  let cursor = start;
@@ -2327,13 +2359,13 @@ var GitTransactionManager = class _GitTransactionManager {
2327
2359
  } catch (error) {
2328
2360
  const code = error.code;
2329
2361
  if (code === "ENOENT") {
2330
- cursor = dirname(cursor);
2362
+ cursor = dirname2(cursor);
2331
2363
  continue;
2332
2364
  }
2333
2365
  if (code === "ENOTEMPTY" || code === "EEXIST") return;
2334
2366
  throw error;
2335
2367
  }
2336
- cursor = dirname(cursor);
2368
+ cursor = dirname2(cursor);
2337
2369
  }
2338
2370
  }
2339
2371
  async #assertSnapshotRef(ref) {
@@ -2353,7 +2385,7 @@ var GitTransactionManager = class _GitTransactionManager {
2353
2385
  // ../../packages/bridge-core/src/storage/sqlite-task-store.ts
2354
2386
  import { mkdirSync } from "node:fs";
2355
2387
  import { createRequire } from "node:module";
2356
- import { dirname as dirname2 } from "node:path";
2388
+ import { dirname as dirname3 } from "node:path";
2357
2389
  var requireNodeBuiltin = createRequire(import.meta.url);
2358
2390
  var optional = (value) => value ?? void 0;
2359
2391
  function parseJson(value, fallback) {
@@ -2445,7 +2477,7 @@ function taskParams(task) {
2445
2477
  var SqliteTaskStore = class {
2446
2478
  #db;
2447
2479
  constructor(filename) {
2448
- if (filename !== ":memory:") mkdirSync(dirname2(filename), { recursive: true, mode: 448 });
2480
+ if (filename !== ":memory:") mkdirSync(dirname3(filename), { recursive: true, mode: 448 });
2449
2481
  const { DatabaseSync } = requireNodeBuiltin("node:sqlite");
2450
2482
  this.#db = new DatabaseSync(filename);
2451
2483
  this.#db.exec(`
@@ -4589,7 +4621,9 @@ ${output2}` : ""}`
4589
4621
 
4590
4622
  // ../../packages/bridge-core/src/bridge/default-control-service.ts
4591
4623
  async function createDefaultControlService(context, environment = process.env) {
4592
- const loaded = await loadVisualDevConfig(context.repoRoot);
4624
+ const loaded = await loadVisualDevConfig(context.repoRoot, {
4625
+ ...context.configRoot === void 0 ? {} : { configRoot: context.configRoot }
4626
+ });
4593
4627
  if (loaded.config.agent.adapter !== "codex") {
4594
4628
  throw new Error(
4595
4629
  `Agent adapter ${loaded.config.agent.adapter} is not implemented in this MVP build`
@@ -4882,6 +4916,7 @@ async function startBridgeCore(options, dependencies) {
4882
4916
  mode: options.mode,
4883
4917
  projectId: loadedConfig.config.project.id,
4884
4918
  repoRoot: loadedConfig.repoRoot,
4919
+ configRoot: loadedConfig.configRoot,
4885
4920
  workspaceRoot: loadedConfig.workspaceRoot,
4886
4921
  upstreamUrl: options.upstreamUrl
4887
4922
  };
@@ -4951,8 +4986,10 @@ async function startBridgeCore(options, dependencies) {
4951
4986
  }
4952
4987
  }
4953
4988
  async function startAttachBridge(options, dependencies = {}) {
4954
- const repoRoot = await discoverGitWorktreeRoot(dependencies.cwd ?? process.cwd());
4955
- const loadedConfig = await loadVisualDevConfig(repoRoot);
4989
+ const cwd = dependencies.cwd ?? process.cwd();
4990
+ const repoRoot = await discoverGitWorktreeRoot(cwd);
4991
+ const configRoot = await discoverVisualDevConfigRoot(cwd, repoRoot);
4992
+ const loadedConfig = await loadVisualDevConfig(repoRoot, { configRoot });
4956
4993
  return await startBridgeCore(
4957
4994
  {
4958
4995
  mode: "attach",
@@ -4966,8 +5003,13 @@ async function startAttachBridge(options, dependencies = {}) {
4966
5003
  );
4967
5004
  }
4968
5005
  async function startManagedBridge(options = {}, dependencies = {}) {
4969
- const repoRoot = await discoverGitWorktreeRoot(dependencies.cwd ?? process.cwd());
4970
- const loadedConfig = await loadVisualDevConfig(repoRoot, { requireConfig: true });
5006
+ const cwd = dependencies.cwd ?? process.cwd();
5007
+ const repoRoot = await discoverGitWorktreeRoot(cwd);
5008
+ const configRoot = await discoverVisualDevConfigRoot(cwd, repoRoot);
5009
+ const loadedConfig = await loadVisualDevConfig(repoRoot, {
5010
+ requireConfig: true,
5011
+ configRoot
5012
+ });
4971
5013
  const command = loadedConfig.config.upstream.command;
4972
5014
  if (command === void 0) {
4973
5015
  throw new Error("visual dev requires upstream.command in .visualdev/config.yaml");
@@ -5076,14 +5118,14 @@ function formatBridgeSummary(bridge) {
5076
5118
 
5077
5119
  // src/doctor.ts
5078
5120
  import { constants } from "node:fs";
5079
- import { access as access2, stat as stat2 } from "node:fs/promises";
5080
- import { delimiter, isAbsolute as isAbsolute5, join as join4, resolve as resolve7 } from "node:path";
5121
+ import { access as access2, stat as stat3 } from "node:fs/promises";
5122
+ import { delimiter, isAbsolute as isAbsolute5, join as join4, relative as relative6, resolve as resolve7 } from "node:path";
5081
5123
  import { execFile as execFile2 } from "node:child_process";
5082
5124
  import { promisify as promisify2 } from "node:util";
5083
5125
  var execFileAsync2 = promisify2(execFile2);
5084
5126
  async function fileExists(path) {
5085
5127
  try {
5086
- await stat2(path);
5128
+ await stat3(path);
5087
5129
  return true;
5088
5130
  } catch (error) {
5089
5131
  if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT") {
@@ -5131,14 +5173,19 @@ async function runDoctor(dependencies = {}) {
5131
5173
  return checks;
5132
5174
  }
5133
5175
  try {
5134
- const loaded = await loadVisualDevConfig(repoRoot);
5176
+ const configRoot = await discoverVisualDevConfigRoot(
5177
+ dependencies.cwd ?? process.cwd(),
5178
+ repoRoot
5179
+ );
5180
+ const loaded = await loadVisualDevConfig(repoRoot, { configRoot });
5135
5181
  checks.push({
5136
5182
  name: "config",
5137
5183
  status: loaded.loadedFiles.length === 0 ? "warning" : "pass",
5138
5184
  message: loaded.loadedFiles.length === 0 ? "No .visualdev/config.yaml; attach defaults are available." : loaded.loadedFiles.join(", ")
5139
5185
  });
5140
5186
  if (await fileExists(loaded.localConfigPath)) {
5141
- const ignored = await isIgnored(repoRoot, ".visualdev/config.local.yaml");
5187
+ const localConfigRelativePath = relative6(repoRoot, loaded.localConfigPath);
5188
+ const ignored = await isIgnored(repoRoot, localConfigRelativePath);
5142
5189
  checks.push({
5143
5190
  name: "local-config-ignore",
5144
5191
  status: ignored ? "pass" : "warning",
@@ -5176,15 +5223,37 @@ function formatDoctorChecks(checks) {
5176
5223
  }
5177
5224
 
5178
5225
  // src/init.ts
5179
- import { readFile as readFile5, mkdir as mkdir3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
5180
- import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
5226
+ import { spawn as spawn6 } from "node:child_process";
5227
+ import { readFile as readFile5, mkdir as mkdir3, realpath as realpath10, stat as stat4, writeFile as writeFile3 } from "node:fs/promises";
5228
+ import { basename as basename2, dirname as dirname4, join as join5, relative as relative7 } from "node:path";
5181
5229
  import { stringify as stringifyYaml } from "yaml";
5230
+ var VITE_CONFIG_FILES = [
5231
+ "vite.config.ts",
5232
+ "vite.config.mts",
5233
+ "vite.config.js",
5234
+ "vite.config.mjs"
5235
+ ];
5236
+ var VITE_IMPORT = 'import { visualRemote } from "visual-remote/vite";';
5237
+ var NEXT_CONFIG_FILES = [
5238
+ "next.config.ts",
5239
+ "next.config.mjs",
5240
+ "next.config.js"
5241
+ ];
5242
+ var NEXT_ESM_IMPORT = 'import { withVisualRemote } from "visual-remote/next";';
5243
+ var NEXT_CJS_IMPORT = 'const { withVisualRemote } = require("visual-remote/next");';
5244
+ var NEXT_CLIENT_MODULE = "visual-remote/next/client";
5245
+ var NEXT_CLIENT_BOOTSTRAP = [
5246
+ 'if (process.env.NODE_ENV === "development") {',
5247
+ ` void import("${NEXT_CLIENT_MODULE}");`,
5248
+ "}",
5249
+ ""
5250
+ ].join("\n");
5182
5251
  function isMissingFile(error) {
5183
5252
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
5184
5253
  }
5185
5254
  async function fileExists2(path) {
5186
5255
  try {
5187
- await stat3(path);
5256
+ await stat4(path);
5188
5257
  return true;
5189
5258
  } catch (error) {
5190
5259
  if (isMissingFile(error)) return false;
@@ -5196,7 +5265,7 @@ function packageManagerFromField(value) {
5196
5265
  const name = value.split("@", 1)[0];
5197
5266
  return name === "bun" || name === "npm" || name === "pnpm" || name === "yarn" ? name : void 0;
5198
5267
  }
5199
- async function detectPackageManager(repoRoot, manifest) {
5268
+ async function detectPackageManager(projectRoot, repoRoot, manifest) {
5200
5269
  const declared = packageManagerFromField(manifest.packageManager);
5201
5270
  if (declared !== void 0) return declared;
5202
5271
  const lockfiles = [
@@ -5206,8 +5275,10 @@ async function detectPackageManager(repoRoot, manifest) {
5206
5275
  ["bun", "bun.lockb"],
5207
5276
  ["npm", "package-lock.json"]
5208
5277
  ];
5209
- for (const [manager, filename] of lockfiles) {
5210
- if (await fileExists2(join5(repoRoot, filename))) return manager;
5278
+ for (const root of projectRoot === repoRoot ? [projectRoot] : [projectRoot, repoRoot]) {
5279
+ for (const [manager, filename] of lockfiles) {
5280
+ if (await fileExists2(join5(root, filename))) return manager;
5281
+ }
5211
5282
  }
5212
5283
  return "npm";
5213
5284
  }
@@ -5217,6 +5288,26 @@ function readDevScript(manifest) {
5217
5288
  }
5218
5289
  return manifest.scripts.dev.trim();
5219
5290
  }
5291
+ function packageRecord(value) {
5292
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
5293
+ }
5294
+ function isViteProject(manifest, devScript) {
5295
+ const packages = {
5296
+ ...packageRecord(manifest.dependencies),
5297
+ ...packageRecord(manifest.devDependencies)
5298
+ };
5299
+ return "vite" in packages || /(^|[\s;&|])vite(?:\s|$)/.test(devScript);
5300
+ }
5301
+ function isNextProject(manifest, devScript) {
5302
+ const packages = {
5303
+ ...packageRecord(manifest.dependencies),
5304
+ ...packageRecord(manifest.devDependencies)
5305
+ };
5306
+ return "next" in packages || /(^|[\s;&|])next(?:\s|$)/.test(devScript);
5307
+ }
5308
+ function hasVisualRemote(manifest) {
5309
+ return "visual-remote" in packageRecord(manifest.dependencies) || "visual-remote" in packageRecord(manifest.devDependencies);
5310
+ }
5220
5311
  function devCommand(manager, script) {
5221
5312
  const command = manager === "pnpm" ? ["corepack", "pnpm", "run", "dev"] : manager === "yarn" ? ["corepack", "yarn", "run", "dev"] : manager === "bun" ? ["bun", "run", "dev"] : ["npm", "run", "dev"];
5222
5313
  const next = /(^|[\s;&|])next(?:\s|$)/.test(script);
@@ -5229,14 +5320,14 @@ function devCommand(manager, script) {
5229
5320
  "{upstreamPort}"
5230
5321
  ];
5231
5322
  }
5232
- async function readManifest(repoRoot) {
5233
- const manifestPath = join5(repoRoot, "package.json");
5323
+ async function readManifest(projectRoot) {
5324
+ const manifestPath = join5(projectRoot, "package.json");
5234
5325
  let source;
5235
5326
  try {
5236
5327
  source = await readFile5(manifestPath, "utf8");
5237
5328
  } catch (error) {
5238
5329
  if (isMissingFile(error)) {
5239
- throw new Error("visual init requires package.json at the Git worktree root");
5330
+ throw new Error("visual init requires package.json in the current directory");
5240
5331
  }
5241
5332
  throw error;
5242
5333
  }
@@ -5251,56 +5342,309 @@ async function readManifest(repoRoot) {
5251
5342
  }
5252
5343
  return value;
5253
5344
  }
5254
- async function initializeVisualDev(dependencies = {}) {
5255
- const repoRoot = await discoverGitWorktreeRoot(dependencies.cwd ?? process.cwd());
5256
- const configPath = join5(repoRoot, CONFIG_PATH);
5257
- if (await fileExists2(configPath)) {
5258
- return { created: false, configPath };
5345
+ async function findViteConfig(projectRoot) {
5346
+ for (const filename of VITE_CONFIG_FILES) {
5347
+ const candidate = join5(projectRoot, filename);
5348
+ if (await fileExists2(candidate)) return candidate;
5259
5349
  }
5260
- const manifest = await readManifest(repoRoot);
5261
- const devScript = readDevScript(manifest);
5262
- const packageManager = await detectPackageManager(repoRoot, manifest);
5263
- const document = {
5264
- version: 1,
5265
- project: {
5266
- id: basename2(repoRoot),
5267
- workspace: "."
5268
- },
5269
- gateway: {
5270
- host: "0.0.0.0",
5271
- port: "auto"
5272
- },
5273
- upstream: {
5274
- port: "auto",
5275
- command: devCommand(packageManager, devScript)
5350
+ throw new Error(
5351
+ "visual init found Vite but could not find vite.config.ts, .mts, .js, or .mjs"
5352
+ );
5353
+ }
5354
+ async function findNextConfig(projectRoot) {
5355
+ for (const filename of NEXT_CONFIG_FILES) {
5356
+ const candidate = join5(projectRoot, filename);
5357
+ if (await fileExists2(candidate)) return candidate;
5358
+ }
5359
+ return join5(projectRoot, "next.config.mjs");
5360
+ }
5361
+ function insertViteImport(source) {
5362
+ const lines = source.split("\n");
5363
+ let index = 0;
5364
+ while (lines[index]?.startsWith("///")) index += 1;
5365
+ lines.splice(index, 0, VITE_IMPORT);
5366
+ return lines.join("\n");
5367
+ }
5368
+ function transformViteConfig(source) {
5369
+ if (source.includes("visual-remote/vite") && /\bvisualRemote\s*\(/.test(source)) {
5370
+ return source;
5371
+ }
5372
+ let transformed = source.includes("visual-remote/vite") ? source : insertViteImport(source);
5373
+ const pluginsPattern = /(\bplugins\s*:\s*\[)/;
5374
+ if (pluginsPattern.test(transformed)) {
5375
+ return transformed.replace(
5376
+ pluginsPattern,
5377
+ (match, _prefix, offset, fullSource) => `${match}visualRemote(),${fullSource[offset + match.length] === "\n" ? "" : " "}`
5378
+ );
5379
+ }
5380
+ const objectConfigPattern = /(defineConfig\s*\(\s*\{)/;
5381
+ if (objectConfigPattern.test(transformed)) {
5382
+ return transformed.replace(
5383
+ objectConfigPattern,
5384
+ "$1\n plugins: [visualRemote()],"
5385
+ );
5386
+ }
5387
+ throw new Error(
5388
+ "visual init could not add visualRemote() to the Vite plugins array"
5389
+ );
5390
+ }
5391
+ async function configureVite(configPath) {
5392
+ const source = await readFile5(configPath, "utf8");
5393
+ const transformed = transformViteConfig(source);
5394
+ if (transformed === source) return false;
5395
+ await writeFile3(configPath, transformed, "utf8");
5396
+ return true;
5397
+ }
5398
+ function expressionEnd(source, expressionStart) {
5399
+ let roundDepth = 0;
5400
+ let squareDepth = 0;
5401
+ let curlyDepth = 0;
5402
+ let quote;
5403
+ let lineComment = false;
5404
+ let blockComment = false;
5405
+ for (let index = expressionStart; index < source.length; index += 1) {
5406
+ const character = source[index];
5407
+ const nextCharacter = source[index + 1];
5408
+ if (lineComment) {
5409
+ if (character === "\n") lineComment = false;
5410
+ continue;
5276
5411
  }
5277
- };
5278
- await mkdir3(dirname3(configPath), { recursive: true });
5412
+ if (blockComment) {
5413
+ if (character === "*" && nextCharacter === "/") {
5414
+ blockComment = false;
5415
+ index += 1;
5416
+ }
5417
+ continue;
5418
+ }
5419
+ if (quote !== void 0) {
5420
+ if (character === "\\") {
5421
+ index += 1;
5422
+ } else if (character === quote) {
5423
+ quote = void 0;
5424
+ }
5425
+ continue;
5426
+ }
5427
+ if (character === "/" && nextCharacter === "/") {
5428
+ lineComment = true;
5429
+ index += 1;
5430
+ continue;
5431
+ }
5432
+ if (character === "/" && nextCharacter === "*") {
5433
+ blockComment = true;
5434
+ index += 1;
5435
+ continue;
5436
+ }
5437
+ if (character === "'" || character === '"' || character === "`") {
5438
+ quote = character;
5439
+ continue;
5440
+ }
5441
+ if (character === "(") roundDepth += 1;
5442
+ if (character === ")") roundDepth -= 1;
5443
+ if (character === "[") squareDepth += 1;
5444
+ if (character === "]") squareDepth -= 1;
5445
+ if (character === "{") curlyDepth += 1;
5446
+ if (character === "}") curlyDepth -= 1;
5447
+ if (character === ";" && roundDepth === 0 && squareDepth === 0 && curlyDepth === 0) {
5448
+ return index;
5449
+ }
5450
+ }
5451
+ return source.length;
5452
+ }
5453
+ function wrapConfigExpression(source, assignmentPattern) {
5454
+ const assignment = assignmentPattern.exec(source);
5455
+ if (assignment?.index === void 0) {
5456
+ throw new Error("visual init could not find the default Next.js config export");
5457
+ }
5458
+ let start = assignment.index + assignment[0].length;
5459
+ while (/\s/.test(source[start] ?? "")) start += 1;
5460
+ const end = expressionEnd(source, start);
5461
+ const rawExpression = source.slice(start, end);
5462
+ const trailingWhitespace = rawExpression.match(/\s*$/)?.[0] ?? "";
5463
+ const expression = rawExpression.slice(0, rawExpression.length - trailingWhitespace.length);
5464
+ if (expression.length === 0) {
5465
+ throw new Error("visual init found an empty Next.js config export");
5466
+ }
5467
+ return `${source.slice(0, start)}withVisualRemote(${expression})${trailingWhitespace}${source.slice(end)}`;
5468
+ }
5469
+ function transformNextConfig(source) {
5470
+ if (source.includes("visual-remote/next") && /\bwithVisualRemote\s*\(/.test(source)) {
5471
+ return source;
5472
+ }
5473
+ if (/\bmodule\.exports\s*=/.test(source)) {
5474
+ const wrapped2 = wrapConfigExpression(source, /\bmodule\.exports\s*=/);
5475
+ return wrapped2.includes(NEXT_CJS_IMPORT) ? wrapped2 : `${NEXT_CJS_IMPORT}
5476
+ ${wrapped2}`;
5477
+ }
5478
+ const wrapped = wrapConfigExpression(source, /\bexport\s+default\b/);
5479
+ return wrapped.includes(NEXT_ESM_IMPORT) ? wrapped : `${NEXT_ESM_IMPORT}
5480
+ ${wrapped}`;
5481
+ }
5482
+ async function configureNext(configPath) {
5483
+ let source;
5279
5484
  try {
5280
- await writeFile3(configPath, stringifyYaml(document), {
5281
- encoding: "utf8",
5282
- flag: "wx"
5283
- });
5485
+ source = await readFile5(configPath, "utf8");
5284
5486
  } catch (error) {
5285
- if (typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST") {
5286
- return { created: false, configPath };
5487
+ if (!isMissingFile(error)) throw error;
5488
+ source = "export default {};\n";
5489
+ }
5490
+ const transformed = transformNextConfig(source);
5491
+ if (transformed === source) return false;
5492
+ await writeFile3(configPath, transformed, "utf8");
5493
+ return true;
5494
+ }
5495
+ async function findNextClientPath(projectRoot) {
5496
+ const candidates = [
5497
+ join5(projectRoot, "instrumentation-client.ts"),
5498
+ join5(projectRoot, "instrumentation-client.js"),
5499
+ join5(projectRoot, "src", "instrumentation-client.ts"),
5500
+ join5(projectRoot, "src", "instrumentation-client.js")
5501
+ ];
5502
+ for (const candidate of candidates) {
5503
+ if (await fileExists2(candidate)) return candidate;
5504
+ }
5505
+ const sourceRoot = await fileExists2(join5(projectRoot, "src")) ? join5(projectRoot, "src") : projectRoot;
5506
+ const extension = await fileExists2(join5(projectRoot, "tsconfig.json")) ? "ts" : "js";
5507
+ return join5(sourceRoot, `instrumentation-client.${extension}`);
5508
+ }
5509
+ async function configureNextClient(clientPath) {
5510
+ let source = "";
5511
+ try {
5512
+ source = await readFile5(clientPath, "utf8");
5513
+ } catch (error) {
5514
+ if (!isMissingFile(error)) throw error;
5515
+ }
5516
+ if (source.includes(NEXT_CLIENT_MODULE)) return false;
5517
+ const separator = source.length === 0 || source.endsWith("\n") ? "" : "\n";
5518
+ await mkdir3(dirname4(clientPath), { recursive: true });
5519
+ await writeFile3(clientPath, `${source}${separator}${NEXT_CLIENT_BOOTSTRAP}`, "utf8");
5520
+ return true;
5521
+ }
5522
+ function installCommand(request) {
5523
+ if (request.packageManager === "pnpm") {
5524
+ return {
5525
+ command: "corepack",
5526
+ args: ["pnpm", "add", "--save-dev", request.packageSpec]
5527
+ };
5528
+ }
5529
+ if (request.packageManager === "yarn") {
5530
+ return {
5531
+ command: "corepack",
5532
+ args: ["yarn", "add", "--dev", request.packageSpec]
5533
+ };
5534
+ }
5535
+ if (request.packageManager === "bun") {
5536
+ return { command: "bun", args: ["add", "--dev", request.packageSpec] };
5537
+ }
5538
+ return {
5539
+ command: "npm",
5540
+ args: ["install", "--save-dev", request.packageSpec]
5541
+ };
5542
+ }
5543
+ async function installPackage(request) {
5544
+ const { command, args } = installCommand(request);
5545
+ await new Promise((resolvePromise, reject) => {
5546
+ const child = spawn6(command, args, {
5547
+ cwd: request.cwd,
5548
+ env: process.env,
5549
+ stdio: "inherit",
5550
+ windowsHide: true
5551
+ });
5552
+ child.once("error", reject);
5553
+ child.once("exit", (code, signal) => {
5554
+ if (code === 0) {
5555
+ resolvePromise();
5556
+ return;
5557
+ }
5558
+ reject(
5559
+ new Error(
5560
+ `${command} ${args.join(" ")} failed${signal === null ? ` with exit code ${code ?? "unknown"}` : ` with ${signal}`}`
5561
+ )
5562
+ );
5563
+ });
5564
+ });
5565
+ }
5566
+ async function initializeVisualDev(dependencies = {}) {
5567
+ const projectRoot = await realpath10(dependencies.cwd ?? process.cwd());
5568
+ const repoRoot = await discoverGitWorktreeRoot(projectRoot);
5569
+ const manifest = await readManifest(projectRoot);
5570
+ const devScript = readDevScript(manifest);
5571
+ const framework = isViteProject(manifest, devScript) ? "vite" : isNextProject(manifest, devScript) ? "next" : void 0;
5572
+ if (framework === void 0) {
5573
+ throw new Error("visual init currently supports Vite and Next.js projects");
5574
+ }
5575
+ const packageManager = await detectPackageManager(projectRoot, repoRoot, manifest);
5576
+ const integrationPath = framework === "vite" ? await findViteConfig(projectRoot) : await findNextConfig(projectRoot);
5577
+ const packageInstalled = !hasVisualRemote(manifest);
5578
+ if (packageInstalled) {
5579
+ await (dependencies.installPackage ?? installPackage)({
5580
+ cwd: projectRoot,
5581
+ packageManager,
5582
+ packageSpec: "visual-remote@latest"
5583
+ });
5584
+ }
5585
+ const integrationChanged = framework === "vite" ? await configureVite(integrationPath) : await configureNext(integrationPath);
5586
+ const clientPath = framework === "next" ? await findNextClientPath(projectRoot) : void 0;
5587
+ const clientChanged = clientPath === void 0 ? void 0 : await configureNextClient(clientPath);
5588
+ const configPath = join5(projectRoot, CONFIG_PATH);
5589
+ const created = !await fileExists2(configPath);
5590
+ if (created) {
5591
+ const document = {
5592
+ version: 1,
5593
+ project: {
5594
+ id: basename2(projectRoot),
5595
+ workspace: "."
5596
+ },
5597
+ gateway: {
5598
+ host: "0.0.0.0",
5599
+ port: "auto"
5600
+ },
5601
+ upstream: {
5602
+ port: "auto",
5603
+ command: devCommand(packageManager, devScript)
5604
+ }
5605
+ };
5606
+ await mkdir3(dirname4(configPath), { recursive: true });
5607
+ try {
5608
+ await writeFile3(configPath, stringifyYaml(document), {
5609
+ encoding: "utf8",
5610
+ flag: "wx"
5611
+ });
5612
+ } catch (error) {
5613
+ if (!isMissingFile(error) && (typeof error !== "object" || error === null || !("code" in error) || error.code !== "EEXIST")) {
5614
+ throw error;
5615
+ }
5287
5616
  }
5288
- throw error;
5289
5617
  }
5290
- return { created: true, configPath, devScript, packageManager };
5618
+ return {
5619
+ framework,
5620
+ created,
5621
+ configPath,
5622
+ devScript,
5623
+ packageManager,
5624
+ integrationPath,
5625
+ integrationChanged,
5626
+ ...clientPath === void 0 || clientChanged === void 0 ? {} : { clientPath, clientChanged },
5627
+ packageInstalled
5628
+ };
5291
5629
  }
5292
5630
  function formatInitResult(result) {
5293
- if (!result.created) {
5294
- return [
5295
- `Already initialized: ${CONFIG_PATH}`,
5296
- "Next: npx --yes visual-remote@latest dev"
5297
- ].join("\n");
5631
+ const configLabel = result.created ? "Created" : "Existing";
5632
+ const integrationLabel = result.integrationChanged ? "Configured" : "Existing";
5633
+ const rows = [
5634
+ `Framework: ${result.framework === "next" ? "Next.js" : "Vite"}`,
5635
+ `${configLabel}: ${relative7(process.cwd(), result.configPath) || CONFIG_PATH}`,
5636
+ `${integrationLabel}: ${relative7(process.cwd(), result.integrationPath)}`
5637
+ ];
5638
+ if (result.clientPath !== void 0) {
5639
+ rows.push(
5640
+ `${result.clientChanged ? "Configured" : "Existing"}: ${relative7(process.cwd(), result.clientPath)}`
5641
+ );
5298
5642
  }
5299
- return [
5300
- `Created: ${CONFIG_PATH}`,
5301
- `Detected: ${result.packageManager} dev (${result.devScript})`,
5302
- "Next: npx --yes visual-remote@latest dev"
5303
- ].join("\n");
5643
+ rows.push(
5644
+ result.packageInstalled ? "Installed: visual-remote@latest" : "Installed: visual-remote",
5645
+ "Next: Start the app normally and open its original URL."
5646
+ );
5647
+ return rows.join("\n");
5304
5648
  }
5305
5649
 
5306
5650
  // src/status.ts
@@ -5353,8 +5697,8 @@ function setExitCode(dependencies, code) {
5353
5697
  }
5354
5698
  }
5355
5699
  function createCli(dependencies = {}) {
5356
- const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.1.3");
5357
- program.command("init").description("Create .visualdev/config.yaml for the current project").action(async () => {
5700
+ const program = new Command().name("visual").description("Visual Remote Dev Bridge").version("0.3.0");
5701
+ program.command("init").description("Configure Visual Remote for the current Vite or Next.js project").action(async () => {
5358
5702
  const result = await initializeVisualDev(dependencies);
5359
5703
  output(dependencies, formatInitResult(result));
5360
5704
  });
@@ -5432,5 +5776,7 @@ export {
5432
5776
  runBridgeUntilSignal,
5433
5777
  runDoctor,
5434
5778
  startAttachBridge,
5435
- startManagedBridge
5779
+ startManagedBridge,
5780
+ transformNextConfig,
5781
+ transformViteConfig
5436
5782
  };