rivet-design 0.17.3 → 0.17.4

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 (77) hide show
  1. package/dist/agent-variants/WorktreeOrchestrator.d.ts +17 -22
  2. package/dist/agent-variants/WorktreeOrchestrator.d.ts.map +1 -1
  3. package/dist/agent-variants/WorktreeOrchestrator.js +126 -214
  4. package/dist/agent-variants/WorktreeOrchestrator.js.map +1 -1
  5. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.d.ts +0 -1
  6. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.d.ts.map +1 -1
  7. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.js +2 -2
  8. package/dist/agent-variants/WorktreeOrchestrator.testHelpers.js.map +1 -1
  9. package/dist/agent-variants/contracts.d.ts +3 -3
  10. package/dist/agent-variants/contracts.d.ts.map +1 -1
  11. package/dist/agent-variants/contracts.js +2 -2
  12. package/dist/agent-variants/contracts.js.map +1 -1
  13. package/dist/cli/commands/libraryOpen.d.ts.map +1 -1
  14. package/dist/cli/commands/libraryOpen.js +5 -1
  15. package/dist/cli/commands/libraryOpen.js.map +1 -1
  16. package/dist/proxy-middleware/preview-bridge.d.ts.map +1 -1
  17. package/dist/proxy-middleware/preview-bridge.js +7 -5
  18. package/dist/proxy-middleware/preview-bridge.js.map +1 -1
  19. package/dist/proxy-middleware/preview-connections.d.ts +9 -0
  20. package/dist/proxy-middleware/preview-connections.d.ts.map +1 -0
  21. package/dist/proxy-middleware/preview-connections.js +30 -0
  22. package/dist/proxy-middleware/preview-connections.js.map +1 -0
  23. package/dist/proxy-middleware/proxy-config.d.ts.map +1 -1
  24. package/dist/proxy-middleware/proxy-config.js +41 -43
  25. package/dist/proxy-middleware/proxy-config.js.map +1 -1
  26. package/dist/routes/agentVariants.d.ts.map +1 -1
  27. package/dist/routes/agentVariants.js +8 -5
  28. package/dist/routes/agentVariants.js.map +1 -1
  29. package/dist/routes/mcp.d.ts.map +1 -1
  30. package/dist/routes/mcp.js +5 -1
  31. package/dist/routes/mcp.js.map +1 -1
  32. package/dist/services/DependencyPreparationService.d.ts +34 -0
  33. package/dist/services/DependencyPreparationService.d.ts.map +1 -0
  34. package/dist/services/DependencyPreparationService.js +344 -0
  35. package/dist/services/DependencyPreparationService.js.map +1 -0
  36. package/dist/services/HistoryReplayService.d.ts +5 -4
  37. package/dist/services/HistoryReplayService.d.ts.map +1 -1
  38. package/dist/services/HistoryReplayService.js +57 -14
  39. package/dist/services/HistoryReplayService.js.map +1 -1
  40. package/dist/services/OperatingSystemServices.d.ts +0 -10
  41. package/dist/services/OperatingSystemServices.d.ts.map +1 -1
  42. package/dist/services/OperatingSystemServices.js +23 -74
  43. package/dist/services/OperatingSystemServices.js.map +1 -1
  44. package/dist/services/ProjectPackageManager.d.ts +35 -0
  45. package/dist/services/ProjectPackageManager.d.ts.map +1 -0
  46. package/dist/services/ProjectPackageManager.js +201 -0
  47. package/dist/services/ProjectPackageManager.js.map +1 -0
  48. package/dist/services/TreeStore.d.ts.map +1 -1
  49. package/dist/services/TreeStore.js +3 -9
  50. package/dist/services/TreeStore.js.map +1 -1
  51. package/dist/services/WorktreeManager.d.ts +8 -10
  52. package/dist/services/WorktreeManager.d.ts.map +1 -1
  53. package/dist/services/WorktreeManager.js +87 -66
  54. package/dist/services/WorktreeManager.js.map +1 -1
  55. package/dist/utils/cloneOrCopyDirSync.d.ts.map +1 -1
  56. package/dist/utils/cloneOrCopyDirSync.js +2 -0
  57. package/dist/utils/cloneOrCopyDirSync.js.map +1 -1
  58. package/dist/utils/devServerError.d.ts +3 -1
  59. package/dist/utils/devServerError.d.ts.map +1 -1
  60. package/dist/utils/devServerError.js +5 -1
  61. package/dist/utils/devServerError.js.map +1 -1
  62. package/dist/utils/htmlDocument.d.ts +4 -5
  63. package/dist/utils/htmlDocument.d.ts.map +1 -1
  64. package/dist/utils/htmlDocument.js +20 -11
  65. package/dist/utils/htmlDocument.js.map +1 -1
  66. package/dist/utils/internalGitIdentity.d.ts +7 -0
  67. package/dist/utils/internalGitIdentity.d.ts.map +1 -0
  68. package/dist/utils/internalGitIdentity.js +17 -0
  69. package/dist/utils/internalGitIdentity.js.map +1 -0
  70. package/dist/utils/portUtils.d.ts +4 -2
  71. package/dist/utils/portUtils.d.ts.map +1 -1
  72. package/dist/utils/portUtils.js +4 -3
  73. package/dist/utils/portUtils.js.map +1 -1
  74. package/package.json +4 -2
  75. package/src/ui/dist/assets/{LibraryApp-PQNnarmo.js → LibraryApp-qbLqcNxM.js} +1 -1
  76. package/src/ui/dist/assets/{main-C7hdsU-C.js → main-touv07Dk.js} +76 -76
  77. package/src/ui/dist/index.html +1 -1
@@ -10,7 +10,9 @@ const events_1 = require("events");
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const os_1 = __importDefault(require("os"));
12
12
  const path_1 = __importDefault(require("path"));
13
- const child_process_1 = require("child_process");
13
+ const DependencyPreparationService_1 = require("../services/DependencyPreparationService");
14
+ const ProjectPackageManager_1 = require("../services/ProjectPackageManager");
15
+ const devServerCommand_1 = require("../utils/devServerCommand");
14
16
  const simple_git_1 = require("simple-git");
15
17
  const logger_1 = require("../utils/logger");
16
18
  const devServerError_1 = require("../utils/devServerError");
@@ -1785,37 +1787,54 @@ class AgentVariantsOrchestrator {
1785
1787
  return this.resources.get(sessionId)?.worktrees.get(workItemId)?.port;
1786
1788
  }
1787
1789
  /**
1788
- * Restores a Rivet-owned variant preview that the active-session idle timer
1789
- * reclaimed. Other missing-preview causes remain terminal so this path never
1790
- * attempts to restart user-owned or repeatedly failing servers.
1790
+ * Restores a Rivet-owned preview after idle reclamation or an explicit retry
1791
+ * of a failed start. Reuses the saved worktree without leasing generation work.
1791
1792
  *
1792
1793
  * @effect Clears the idle marker and prior crash budget, starts the worktree
1793
1794
  * dev server, and emits preview-state snapshots.
1794
1795
  * @deps Uses the live session resources, project context, and dev-server
1795
1796
  * manager owned by this orchestrator.
1796
1797
  */
1797
- async restoreVariantPreview(sessionId, workItemId) {
1798
+ async restoreVariantPreview(sessionId, workItemId, options = {}) {
1798
1799
  const resources = this.resources.get(sessionId);
1799
1800
  const record = resources?.worktrees.get(workItemId);
1800
- if (!resources || !record)
1801
+ if (!resources || !record || resources.cleanupStarted)
1801
1802
  return undefined;
1803
+ if (record.previewRestoration)
1804
+ return record.previewRestoration;
1802
1805
  if (record.port !== undefined && record.devServerProcess) {
1803
1806
  this.touchSession(sessionId);
1804
1807
  return record.port;
1805
1808
  }
1806
- if (resources.previewFailures.get(workItemId)?.reason !== 'idle_timeout') {
1809
+ const failure = resources.previewFailures.get(workItemId);
1810
+ const isSucceeded = this.store
1811
+ .getVariants(sessionId)
1812
+ .some((variant) => variant.workItemId === workItemId && variant.status === 'succeeded');
1813
+ const isExplicitRetry = options.retryFailed === true &&
1814
+ failure?.reason !== 'missing_static_entry';
1815
+ if (!failure ||
1816
+ !isSucceeded ||
1817
+ (failure.reason !== 'idle_timeout' && !isExplicitRetry)) {
1807
1818
  return undefined;
1808
1819
  }
1809
1820
  resources.previewFailures.delete(workItemId);
1810
1821
  record.devServerAutoRestarts = 0;
1811
1822
  this.emitChange();
1812
1823
  const projectContext = this.store.getProjectContext(sessionId);
1813
- return this.startVariantDevServer({
1824
+ const restoration = this.startVariantDevServer({
1814
1825
  sessionId,
1815
1826
  workItemId,
1816
1827
  record,
1817
1828
  isFresh: projectContext.kind === 'fresh',
1818
1829
  });
1830
+ record.previewRestoration = restoration;
1831
+ try {
1832
+ return await restoration;
1833
+ }
1834
+ finally {
1835
+ if (record.previewRestoration === restoration)
1836
+ record.previewRestoration = undefined;
1837
+ }
1819
1838
  }
1820
1839
  /**
1821
1840
  * @effect Resolves the dev server port for a preview request and refreshes
@@ -1958,8 +1977,8 @@ class AgentVariantsOrchestrator {
1958
1977
  });
1959
1978
  this.emitChange();
1960
1979
  // Await provisioning before returning so request_work always sees a
1961
- // populated worktreePath on each leased item. Provisioning is fast
1962
- // (`git worktree add` per variant + node_modules symlink), and the
1980
+ // populated worktreePath on each leased item. Provisioning creates a
1981
+ // git worktree and copies dependencies for each variant, and the
1963
1982
  // agent has nothing useful to do between approve and request_work
1964
1983
  // anyway — running them serially is the contract the skill assumes.
1965
1984
  try {
@@ -2972,12 +2991,6 @@ class AgentVariantsOrchestrator {
2972
2991
  // because teardown runs AFTER the rename — too late to prevent the
2973
2992
  // stub-dir race.
2974
2993
  await this.stopAllDevServersForSession(resources);
2975
- // node_modules in fresh variants 1..N is a symlink into variant 0
2976
- // (see runBackgroundInstall). After rename, variant 0's
2977
- // node_modules vanishes during cleanup of the sibling worktrees,
2978
- // leaving the chosen variant with a dangling link. Resolve the
2979
- // symlink to a real directory first so the destination has real
2980
- // deps and the rename is self-contained.
2981
2994
  isDependencyInstallRequired = await resolveNodeModulesSymlink(record.worktreePath);
2982
2995
  await this.renameWorktreeIntoDestination(record.worktreePath, destinationPath);
2983
2996
  usedRename = true;
@@ -3404,8 +3417,7 @@ class AgentVariantsOrchestrator {
3404
3417
  }
3405
3418
  /**
3406
3419
  * Existing-project flow: clone the user's repo N times via git worktree.
3407
- * Runs synchronously because the operation is fast (no install needed —
3408
- * node_modules is symlinked from the source project).
3420
+ * Waits for source and dependency copies before making work available.
3409
3421
  */
3410
3422
  async provisionExistingWorktrees(sessionId, approveResult) {
3411
3423
  const resources = this.ensureResources(sessionId);
@@ -3545,24 +3557,17 @@ class AgentVariantsOrchestrator {
3545
3557
  async runBackgroundInstall(sessionId, scaffoldWorkItemId, worktreePaths, scaffoldStartedAt) {
3546
3558
  const installStartedAt = Date.now();
3547
3559
  try {
3548
- const [firstWorktree, ...rest] = worktreePaths;
3549
- await this.installDependencies(firstWorktree);
3550
- await Promise.all(rest.map(async (p) => {
3551
- const target = path_1.default.join(firstWorktree, 'node_modules');
3552
- const link = path_1.default.join(p, 'node_modules');
3553
- // The fresh template has no node_modules until install runs, so
3554
- // the link target is guaranteed absent in `p` — no need to remove
3555
- // first.
3556
- try {
3557
- fs_1.default.symlinkSync(target, link, 'dir');
3558
- }
3559
- catch (symlinkErr) {
3560
- log.warn(`Symlink node_modules failed for ${p}, falling back to install`, symlinkErr);
3561
- await this.installDependencies(p);
3562
- }
3563
- }));
3560
+ await Promise.all(worktreePaths.map((worktreePath) => this.installDependencies(worktreePath, {
3561
+ workspaceRoot: worktreePath,
3562
+ signal: this.resources.get(sessionId)?.dependencyAbortController.signal,
3563
+ })));
3564
3564
  }
3565
3565
  catch (err) {
3566
+ // Teardown aborts in-flight preparation; a cancelled session is not a
3567
+ // scaffold failure.
3568
+ const resources = this.resources.get(sessionId);
3569
+ if (!resources || resources.cleanupStarted)
3570
+ return;
3566
3571
  const message = err instanceof Error ? err.message : String(err);
3567
3572
  log.warn(`npm install failed for session ${sessionId}; marking scaffold_base failed`, err);
3568
3573
  trackScaffoldFailed(this.telemetry, {
@@ -3845,8 +3850,10 @@ class AgentVariantsOrchestrator {
3845
3850
  let port;
3846
3851
  try {
3847
3852
  port = await this.worktrees.findFreePortFrom(scanFrom);
3848
- const dev = await this.resolveDevServer(args.sessionId, args.workItemId, args.record.worktreePath, port, args.isFresh);
3853
+ const dev = await this.resolveDevServer(args.sessionId, args.record.worktreePath, port, args.isFresh);
3849
3854
  lastDev = dev;
3855
+ if (!canContinueStart())
3856
+ return undefined;
3850
3857
  const proc = await this.worktrees.startDevServer(dev.cwd, port, dev.cmd, dev.args, dev.env);
3851
3858
  // The boot can outlive the session (a cancel during the readiness
3852
3859
  // wait, or during an auto-restart's delay). At this point the child
@@ -3982,13 +3989,33 @@ class AgentVariantsOrchestrator {
3982
3989
  if (missingDeps && lastDev) {
3983
3990
  installRetried = true;
3984
3991
  try {
3985
- await this.installDependencies(lastDev.cwd, lastDev.packageManager);
3992
+ await this.installDependencies(lastDev.cwd, {
3993
+ workspaceRoot: args.record.worktreePath,
3994
+ force: true,
3995
+ signal: this.resources.get(args.sessionId)
3996
+ ?.dependencyAbortController.signal,
3997
+ });
3986
3998
  }
3987
3999
  catch (installErr) {
3988
- // Still retry: the boot error may have been transient, and the
3989
- // retry's own failure telemetry is more specific than aborting
3990
- // here on the install's stderr.
3991
- log.warn(`Dependency install before dev-server retry failed for variant ${args.workItemId}`, installErr);
4000
+ log.warn(`Dependency preparation failed for variant ${args.workItemId}`, installErr);
4001
+ this.resources
4002
+ .get(args.sessionId)
4003
+ ?.previewFailures.set(args.workItemId, {
4004
+ reason: 'dependency_install_failed',
4005
+ portInUse: false,
4006
+ });
4007
+ trackPreviewFailed(this.telemetry, {
4008
+ sessionId: args.sessionId,
4009
+ variantId: args.workItemId,
4010
+ projectContextKind,
4011
+ previewKind: 'dev_server',
4012
+ errorCode: 'DEPENDENCY_INSTALL_FAILED',
4013
+ reason: 'dependency_install_failed',
4014
+ portInUse: false,
4015
+ durationMs: Date.now() - previewStartedAt,
4016
+ });
4017
+ this.emitChange();
4018
+ return undefined;
3992
4019
  }
3993
4020
  }
3994
4021
  continue;
@@ -4002,9 +4029,11 @@ class AgentVariantsOrchestrator {
4002
4029
  reason,
4003
4030
  portInUse,
4004
4031
  });
4005
- const errorCode = reason === 'missing_dev_script'
4006
- ? 'DEV_SCRIPT_MISSING'
4007
- : 'DEV_SERVER_START_FAILED';
4032
+ let errorCode = 'DEV_SERVER_START_FAILED';
4033
+ if (reason === 'dependency_install_failed')
4034
+ errorCode = 'DEPENDENCY_INSTALL_FAILED';
4035
+ if (reason === 'missing_dev_script')
4036
+ errorCode = 'DEV_SCRIPT_MISSING';
4008
4037
  // Telemetry gets structured classification only — never the raw
4009
4038
  // output tail, which routinely carries absolute project paths and
4010
4039
  // source frames (see AGENTS.md "What NEVER to capture"). The full
@@ -4875,16 +4904,13 @@ class AgentVariantsOrchestrator {
4875
4904
  * always use the Vite template's npm command at the worktree root; existing
4876
4905
  * projects defer to the user's framework/packageManager config.
4877
4906
  */
4878
- async resolveDevServer(sessionId, workItemId, worktreePath, port, isFresh) {
4907
+ async resolveDevServer(sessionId, worktreePath, port, isFresh) {
4879
4908
  const resolved = await this.resolveDevServerCommand(sessionId, worktreePath, port, isFresh);
4880
- if (!isFresh) {
4881
- await this.ensureExistingProjectDependencies({
4882
- sessionId,
4883
- workItemId,
4884
- cwd: resolved.cwd,
4885
- packageManager: resolved.packageManager,
4886
- });
4887
- }
4909
+ await this.ensureProjectDependencies({
4910
+ signal: this.resources.get(sessionId)?.dependencyAbortController.signal,
4911
+ cwd: resolved.cwd,
4912
+ workspaceRoot: worktreePath,
4913
+ });
4888
4914
  return resolved;
4889
4915
  }
4890
4916
  /**
@@ -4917,18 +4943,31 @@ class AgentVariantsOrchestrator {
4917
4943
  throw new Error('Static projects do not have a dev server command');
4918
4944
  }
4919
4945
  const cwd = await this.resolveExistingProjectCwd(sessionId, worktreePath, env);
4920
- const packageManager = detectPackageManagerForCwd(cwd, env.packageManager);
4921
4946
  // Every branch below — including the production buildDevCommand recipes
4922
4947
  // from utils/devServerCommand.ts — invokes the package.json script named
4923
4948
  // by env.devCommand, so the preflight applies to all of them.
4924
4949
  if (env.devCommand) {
4925
4950
  assertDevScriptExists(cwd, env.devCommand);
4926
4951
  }
4952
+ const detected = fs_1.default.existsSync(path_1.default.join(cwd, 'package.json'))
4953
+ ? await (0, ProjectPackageManager_1.resolveProjectPackageManager)({
4954
+ cwd,
4955
+ workspaceRoot: worktreePath,
4956
+ }).catch((error) => {
4957
+ throw new devServerError_1.DevServerStartError({
4958
+ message: error instanceof Error ? error.message : String(error),
4959
+ detail: '',
4960
+ reason: 'dependency_install_failed',
4961
+ portInUse: false,
4962
+ });
4963
+ })
4964
+ : null;
4965
+ const packageManager = detected?.name ?? env.packageManager;
4927
4966
  if (packageManager !== env.packageManager) {
4928
4967
  return {
4929
4968
  cwd,
4930
4969
  packageManager,
4931
- ...buildExistingProjectDevCommand(packageManager, env.devCommand, port),
4970
+ ...(0, devServerCommand_1.buildDevServerCommand)(env.framework, packageManager, env.devCommand, port),
4932
4971
  };
4933
4972
  }
4934
4973
  if (env.buildDevCommand) {
@@ -4942,19 +4981,14 @@ class AgentVariantsOrchestrator {
4942
4981
  packageManager,
4943
4982
  };
4944
4983
  }
4945
- /**
4946
- * Existing-project variants usually reuse the user's installed
4947
- * node_modules. Nested example projects may be standalone packages with no
4948
- * install in the cloned worktree; install only when the package has no
4949
- * node_modules so normal user projects are not penalized.
4950
- */
4951
- async ensureExistingProjectDependencies(args) {
4952
- const packageJsonPath = path_1.default.join(args.cwd, 'package.json');
4953
- const nodeModulesPath = path_1.default.join(args.cwd, 'node_modules');
4954
- if (!fs_1.default.existsSync(packageJsonPath) || fs_1.default.existsSync(nodeModulesPath)) {
4984
+ /** Prepares the direction's declared dependencies before each server start. */
4985
+ async ensureProjectDependencies(args) {
4986
+ if (!fs_1.default.existsSync(path_1.default.join(args.cwd, 'package.json')))
4955
4987
  return;
4956
- }
4957
- await this.installDependencies(args.cwd, args.packageManager);
4988
+ await this.installDependencies(args.cwd, {
4989
+ workspaceRoot: args.workspaceRoot,
4990
+ signal: args.signal,
4991
+ });
4958
4992
  }
4959
4993
  /**
4960
4994
  * Resolve the dev-server working directory for an existing-project variant.
@@ -4983,8 +5017,7 @@ class AgentVariantsOrchestrator {
4983
5017
  /**
4984
5018
  * After the chosen variant has been renamed into `destinationPath`, move
4985
5019
  * each unchosen sibling worktree into a `<destinationParent>/<slug>-variants/NN-<label>/`
4986
- * folder and repoint its `node_modules` symlink at the chosen project's
4987
- * real install. Writes a manifest documenting all 5 variants (the chosen
5020
+ * folder with its own prepared dependency installation. Writes a manifest documenting all 5 variants (the chosen
4988
5021
  * one with `folder: null` and `chosenPath: '../<slug>'`).
4989
5022
  *
4990
5023
  * Sets `vitePreservedSiblings = true` on session resources so the
@@ -4999,7 +5032,6 @@ class AgentVariantsOrchestrator {
4999
5032
  const destinationParent = path_1.default.dirname(args.destinationPath);
5000
5033
  const projectSlug = path_1.default.basename(args.destinationPath);
5001
5034
  const historyDir = (0, createProjectArtifacts_1.createVariantsHistoryPath)(destinationParent, projectSlug);
5002
- const sharedNodeModulesAbs = path_1.default.join(args.destinationPath, 'node_modules');
5003
5035
  const briefs = this.store.getBriefs(args.sessionId);
5004
5036
  const variants = this.store.getVariants(args.sessionId);
5005
5037
  const briefIndexById = new Map(briefs.map((b, i) => [b.briefId, i]));
@@ -5057,26 +5089,6 @@ class AgentVariantsOrchestrator {
5057
5089
  fs_1.default.renameSync(record.worktreePath, dest);
5058
5090
  return dest;
5059
5091
  })();
5060
- // Repoint node_modules at the chosen project's real install.
5061
- try {
5062
- if (this.worktrees.repointNodeModulesSymlink) {
5063
- this.worktrees.repointNodeModulesSymlink(newPath, sharedNodeModulesAbs);
5064
- }
5065
- else {
5066
- const link = path_1.default.join(newPath, 'node_modules');
5067
- try {
5068
- fs_1.default.rmSync(link, { force: true, recursive: true });
5069
- }
5070
- catch {
5071
- /* may not exist */
5072
- }
5073
- const target = path_1.default.relative(path_1.default.dirname(link), sharedNodeModulesAbs);
5074
- fs_1.default.symlinkSync(target, link, 'dir');
5075
- }
5076
- }
5077
- catch (err) {
5078
- log.warn(`repointNodeModulesSymlink failed for ${newPath}; variant still runnable after npm install`, err);
5079
- }
5080
5092
  // Update the in-memory record so teardown doesn't try to operate on
5081
5093
  // the stale path.
5082
5094
  record.worktreePath = newPath;
@@ -5408,6 +5420,8 @@ class AgentVariantsOrchestrator {
5408
5420
  resources.cleanupStarted = true;
5409
5421
  this.clearActivePreviewIdleTimer(resources);
5410
5422
  log.info(`Tearing down session ${sessionId} (reason: ${reason})`);
5423
+ resources.dependencyAbortController.abort();
5424
+ await Promise.all([...resources.worktrees.values()].map((record) => DependencyPreparationService_1.previewDependencies.settle(record.worktreePath)));
5411
5425
  // Drain in-flight history persists BEFORE deleting worktrees: a tree
5412
5426
  // snapshot's `git add -A --work-tree <worktree>` racing the deletion
5413
5427
  // below doesn't fail — it stages whatever files remain, publishing a
@@ -5504,6 +5518,7 @@ class AgentVariantsOrchestrator {
5504
5518
  workspaceManager: new VariantWorkspaceManager_1.VariantWorkspaceManager(),
5505
5519
  contextAssets: new Map(),
5506
5520
  cleanupStarted: false,
5521
+ dependencyAbortController: new AbortController(),
5507
5522
  committedVariantIds: new Set(),
5508
5523
  startedAt: Date.now(),
5509
5524
  nextRoundIndex: 0,
@@ -6447,54 +6462,15 @@ function copyStaticPreviewDirectory(src, dest) {
6447
6462
  }
6448
6463
  }
6449
6464
  }
6450
- const buildInstallCommand = (packageManager) => {
6451
- if (packageManager === 'npm') {
6452
- return {
6453
- cmd: 'npm',
6454
- args: ['install', '--no-audit', '--no-fund', '--ignore-scripts'],
6455
- };
6456
- }
6457
- return {
6458
- cmd: packageManager,
6459
- args: ['install', '--ignore-scripts'],
6460
- };
6461
- };
6462
- const defaultInstallDependencies = (worktreePath, packageManager = 'npm') => {
6463
- const install = buildInstallCommand(packageManager);
6464
- return new Promise((resolve, reject) => {
6465
- const proc = (0, child_process_1.spawn)(install.cmd, install.args, {
6466
- cwd: worktreePath,
6467
- stdio: ['ignore', 'ignore', 'pipe'],
6468
- });
6469
- let stderr = '';
6470
- proc.stderr.on('data', (chunk) => {
6471
- stderr += chunk.toString();
6472
- });
6473
- proc.on('error', reject);
6474
- proc.on('exit', (code) => {
6475
- if (code === 0) {
6476
- resolve();
6477
- return;
6478
- }
6479
- reject(new Error(`${install.cmd} install in ${worktreePath} failed (code ${code}): ${stderr.slice(-512)}`));
6480
- });
6465
+ /** Prepares an isolated project through its selected package manager. */
6466
+ const defaultInstallDependencies = async (worktreePath, options) => {
6467
+ await DependencyPreparationService_1.previewDependencies.prepare({
6468
+ cwd: worktreePath,
6469
+ workspaceRoot: options?.workspaceRoot ?? worktreePath,
6470
+ force: options?.force,
6471
+ signal: options?.signal,
6481
6472
  });
6482
6473
  };
6483
- const detectPackageManagerForCwd = (cwd, fallback) => {
6484
- if (fs_1.default.existsSync(path_1.default.join(cwd, 'yarn.lock')))
6485
- return 'yarn';
6486
- if (fs_1.default.existsSync(path_1.default.join(cwd, 'pnpm-lock.yaml')))
6487
- return 'pnpm';
6488
- if (fs_1.default.existsSync(path_1.default.join(cwd, 'package-lock.json')))
6489
- return 'npm';
6490
- if (fallback === 'pnpm' || fallback === 'yarn' || fallback === 'npm') {
6491
- // A nested package without a lockfile should not inherit a root Yarn Berry
6492
- // workspace when it is not declared as a workspace member; npm treats it as
6493
- // an independent package and is the safest default for old examples.
6494
- return fs_1.default.existsSync(path_1.default.join(cwd, 'package.json')) ? 'npm' : fallback;
6495
- }
6496
- return 'npm';
6497
- };
6498
6474
  /**
6499
6475
  * Turn the invocation a live dev server actually booted with into a
6500
6476
  * port-parameterized replay recipe. The resolution bakes the allocated port
@@ -6516,12 +6492,16 @@ const buildServeRecipeFromResolved = (snapshotRoot, dev, port) => {
6516
6492
  };
6517
6493
  /**
6518
6494
  * Whether an install at `cwd` stays inside the Rivet-owned worktree.
6519
- * Existing-project worktrees SYMLINK `node_modules` to the user's real
6520
- * install (WorktreeManager.symlinkNodeModulesIfAvailable) — running a
6521
- * package manager through that link would mutate the user's project
6522
- * dependencies, which the variants flow must never do. Install is safe only
6495
+ * A linked `node_modules` can point at another project's install; running a
6496
+ * package manager through it would mutate dependencies outside this worktree.
6497
+ * Install is safe only
6523
6498
  * when `node_modules` is absent (a nested standalone package — the install
6524
6499
  * creates a worktree-local dir) or a real directory owned by the worktree.
6500
+ *
6501
+ * Complements DependencyPreparationService.assertOwnedInstall rather than
6502
+ * duplicating it: this stricter check (any symlink, even one resolving
6503
+ * inside the worktree) decides whether an install RETRY is worth attempting,
6504
+ * while the service enforces containment on every install it runs.
6525
6505
  */
6526
6506
  const canInstallWithoutTouchingHost = (cwd) => {
6527
6507
  try {
@@ -6568,29 +6548,6 @@ const assertDevScriptExists = (cwd, scriptName) => {
6568
6548
  : '',
6569
6549
  });
6570
6550
  };
6571
- const buildExistingProjectDevCommand = (packageManager, devCommand, port) => {
6572
- const env = { PORT: String(port) };
6573
- if (packageManager === 'npm') {
6574
- return {
6575
- cmd: 'npm',
6576
- args: [
6577
- 'run',
6578
- devCommand,
6579
- '--',
6580
- '--port',
6581
- String(port),
6582
- '--host',
6583
- FRESH_DEV_SERVER_HOST,
6584
- ],
6585
- env,
6586
- };
6587
- }
6588
- return {
6589
- cmd: packageManager,
6590
- args: [devCommand, '--port', String(port), '--host', FRESH_DEV_SERVER_HOST],
6591
- env,
6592
- };
6593
- };
6594
6551
  /**
6595
6552
  * True when `a` and `b` (or the closest existing ancestor of each) live on
6596
6553
  * the same filesystem volume. Used by commitVariant to decide between an
@@ -6612,65 +6569,20 @@ function areSameVolume(a, b) {
6612
6569
  return false;
6613
6570
  }
6614
6571
  }
6615
- /**
6616
- * Materialize node_modules from symlink → real directory at the given path,
6617
- * so a subsequent rename of that path leaves a self-contained tree.
6618
- *
6619
- * Fresh worktrees 1..N share variant 0's node_modules via a symlink (see
6620
- * runBackgroundInstall). When the user picks variant K and we rename it
6621
- * into the destination, the sibling worktrees get torn down — which would
6622
- * otherwise leave a dangling symlink in the destination if variant K was
6623
- * the one with the symlink (K > 0).
6624
- *
6625
- * Returns true when the destination must run a dependency install after the
6626
- * rename because node_modules is missing or points nowhere.
6627
- */
6572
+ /** Clears a borrowed install so a moved generated project prepares its own dependencies. */
6628
6573
  async function resolveNodeModulesSymlink(worktreePath) {
6629
6574
  const nodeModulesPath = path_1.default.join(worktreePath, 'node_modules');
6630
- let lstat;
6631
6575
  try {
6632
- lstat = await fs_1.default.promises.lstat(nodeModulesPath);
6633
- }
6634
- catch {
6635
- return true;
6636
- }
6637
- if (!lstat.isSymbolicLink())
6638
- return false;
6639
- let target;
6640
- try {
6641
- target = await fs_1.default.promises.realpath(nodeModulesPath);
6642
- }
6643
- catch {
6576
+ if (!(await fs_1.default.promises.lstat(nodeModulesPath)).isSymbolicLink())
6577
+ return false;
6644
6578
  await fs_1.default.promises.rm(nodeModulesPath, { force: true });
6645
6579
  return true;
6646
6580
  }
6647
- // Stage the copy under a sibling temp name so a mid-copy failure (disk
6648
- // full, EACCES, etc.) leaves the original symlink intact rather than
6649
- // wiping it and leaving the worktree with no node_modules at all.
6650
- // The final swap is then atomic on the same filesystem (rename + unlink).
6651
- const stagingPath = `${nodeModulesPath}.materializing-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
6652
- try {
6653
- await fs_1.default.promises.cp(target, stagingPath, {
6654
- recursive: true,
6655
- dereference: false,
6656
- });
6581
+ catch (error) {
6582
+ if (error.code === 'ENOENT')
6583
+ return true;
6584
+ throw error;
6657
6585
  }
6658
- catch (err) {
6659
- // Best-effort cleanup of the partial staging dir; leave the symlink
6660
- // intact so the caller can re-run materialization.
6661
- await fs_1.default.promises
6662
- .rm(stagingPath, { recursive: true, force: true })
6663
- .catch(() => { });
6664
- throw err;
6665
- }
6666
- // Swap: remove the symlink, then rename the staging dir into place.
6667
- // Both operations are local fs ops on the same parent — fast and
6668
- // unlikely to fail independently, but if the rename fails the symlink
6669
- // is already gone, so prefer to leave the staging dir on disk over a
6670
- // half-broken worktree (the caller will surface the throw).
6671
- await fs_1.default.promises.rm(nodeModulesPath, { force: true });
6672
- await fs_1.default.promises.rename(stagingPath, nodeModulesPath);
6673
- return false;
6674
6586
  }
6675
6587
  const defaultMaterializeProject = async (sourceWorktreePath, destinationPath) => {
6676
6588
  await fs_1.default.promises.mkdir(destinationPath, { recursive: true });