@specific.dev/cli 0.1.46 → 0.1.48

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 (31) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.__PAGE__.txt +2 -2
  4. package/dist/admin/__next._full.txt +2 -2
  5. package/dist/admin/__next._head.txt +1 -1
  6. package/dist/admin/__next._index.txt +1 -1
  7. package/dist/admin/__next._tree.txt +1 -1
  8. package/dist/admin/_next/static/chunks/{e13659c7ad8234ce.js → 75cb455f07e7651a.js} +1 -1
  9. package/dist/admin/_not-found/__next._full.txt +1 -1
  10. package/dist/admin/_not-found/__next._head.txt +1 -1
  11. package/dist/admin/_not-found/__next._index.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  14. package/dist/admin/_not-found/__next._tree.txt +1 -1
  15. package/dist/admin/_not-found/index.html +1 -1
  16. package/dist/admin/_not-found/index.txt +1 -1
  17. package/dist/admin/databases/__next._full.txt +1 -1
  18. package/dist/admin/databases/__next._head.txt +1 -1
  19. package/dist/admin/databases/__next._index.txt +1 -1
  20. package/dist/admin/databases/__next._tree.txt +1 -1
  21. package/dist/admin/databases/__next.databases.__PAGE__.txt +1 -1
  22. package/dist/admin/databases/__next.databases.txt +1 -1
  23. package/dist/admin/databases/index.html +1 -1
  24. package/dist/admin/databases/index.txt +1 -1
  25. package/dist/admin/index.html +1 -1
  26. package/dist/admin/index.txt +2 -2
  27. package/dist/cli.js +630 -403
  28. package/package.json +7 -2
  29. /package/dist/admin/_next/static/{uXSe8Dmoqn0jmhvY6Iln0 → dyH4SZNKyN31L1iV-yPZA}/_buildManifest.js +0 -0
  30. /package/dist/admin/_next/static/{uXSe8Dmoqn0jmhvY6Iln0 → dyH4SZNKyN31L1iV-yPZA}/_clientMiddlewareManifest.json +0 -0
  31. /package/dist/admin/_next/static/{uXSe8Dmoqn0jmhvY6Iln0 → dyH4SZNKyN31L1iV-yPZA}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -238,15 +238,15 @@ var init_wsl_utils = __esm({
238
238
  const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
239
239
  return stdout.trim();
240
240
  };
241
- convertWslPathToWindows = async (path22) => {
242
- if (/^[a-z]+:\/\//i.test(path22)) {
243
- return path22;
241
+ convertWslPathToWindows = async (path23) => {
242
+ if (/^[a-z]+:\/\//i.test(path23)) {
243
+ return path23;
244
244
  }
245
245
  try {
246
- const { stdout } = await execFile2("wslpath", ["-aw", path22], { encoding: "utf8" });
246
+ const { stdout } = await execFile2("wslpath", ["-aw", path23], { encoding: "utf8" });
247
247
  return stdout.trim();
248
248
  } catch {
249
- return path22;
249
+ return path23;
250
250
  }
251
251
  };
252
252
  }
@@ -754,8 +754,8 @@ var require_dist = __commonJS({
754
754
  var $global, $module, $NaN = NaN;
755
755
  if ("undefined" != typeof window ? $global = window : "undefined" != typeof self ? $global = self : "undefined" != typeof global ? ($global = global).require = __require : $global = this, void 0 === $global || void 0 === $global.Array) throw new Error("no global object found");
756
756
  if ("undefined" != typeof module && ($module = module), !$global.fs && $global.require) try {
757
- var fs24 = $global.require("fs");
758
- "object" == typeof fs24 && null !== fs24 && 0 !== Object.keys(fs24).length && ($global.fs = fs24);
757
+ var fs25 = $global.require("fs");
758
+ "object" == typeof fs25 && null !== fs25 && 0 !== Object.keys(fs25).length && ($global.fs = fs25);
759
759
  } catch (e) {
760
760
  }
761
761
  if (!$global.fs) {
@@ -183873,7 +183873,7 @@ function trackEvent(event, properties) {
183873
183873
  event,
183874
183874
  properties: {
183875
183875
  ...properties,
183876
- cli_version: "0.1.46",
183876
+ cli_version: "0.1.48",
183877
183877
  platform: process.platform,
183878
183878
  node_version: process.version,
183879
183879
  project_id: getProjectId(),
@@ -184166,11 +184166,11 @@ import { join as join6, dirname as dirname2 } from "path";
184166
184166
  import { fileURLToPath as fileURLToPath2 } from "url";
184167
184167
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
184168
184168
  var docsDir = join6(__dirname2, "docs");
184169
- function docsCommand(path22) {
184170
- const docPath = resolveDocPath(path22);
184169
+ function docsCommand(path23) {
184170
+ const docPath = resolveDocPath(path23);
184171
184171
  if (!docPath) {
184172
184172
  console.error(
184173
- `Documentation not found: ${path22 || "index"}
184173
+ `Documentation not found: ${path23 || "index"}
184174
184174
 
184175
184175
  Run 'specific docs' to see available topics.`
184176
184176
  );
@@ -184179,16 +184179,16 @@ Run 'specific docs' to see available topics.`
184179
184179
  const content = readFileSync5(docPath, "utf-8");
184180
184180
  console.log(content);
184181
184181
  }
184182
- function resolveDocPath(path22) {
184183
- if (!path22) {
184182
+ function resolveDocPath(path23) {
184183
+ if (!path23) {
184184
184184
  const indexPath2 = join6(docsDir, "index.md");
184185
184185
  return existsSync5(indexPath2) ? indexPath2 : null;
184186
184186
  }
184187
- const directPath = join6(docsDir, `${path22}.md`);
184187
+ const directPath = join6(docsDir, `${path23}.md`);
184188
184188
  if (existsSync5(directPath)) {
184189
184189
  return directPath;
184190
184190
  }
184191
- const indexPath = join6(docsDir, path22, "index.md");
184191
+ const indexPath = join6(docsDir, path23, "index.md");
184192
184192
  if (existsSync5(indexPath)) {
184193
184193
  return indexPath;
184194
184194
  }
@@ -184767,8 +184767,8 @@ function checkCommand() {
184767
184767
  import React6, { useState as useState5, useEffect as useEffect3, useRef } from "react";
184768
184768
  import { render as render4, Text as Text6, Box as Box6, useApp as useApp2, Static } from "ink";
184769
184769
  import Spinner4 from "ink-spinner";
184770
- import * as fs19 from "fs";
184771
- import * as path16 from "path";
184770
+ import * as fs20 from "fs";
184771
+ import * as path17 from "path";
184772
184772
 
184773
184773
  // node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.js
184774
184774
  import { EventEmitter } from "node:events";
@@ -184860,7 +184860,7 @@ var ReaddirpStream = class extends Readable {
184860
184860
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
184861
184861
  const statMethod = opts.lstat ? lstat : stat;
184862
184862
  if (wantBigintFsStats) {
184863
- this._stat = (path22) => statMethod(path22, { bigint: true });
184863
+ this._stat = (path23) => statMethod(path23, { bigint: true });
184864
184864
  } else {
184865
184865
  this._stat = statMethod;
184866
184866
  }
@@ -184885,8 +184885,8 @@ var ReaddirpStream = class extends Readable {
184885
184885
  const par = this.parent;
184886
184886
  const fil = par && par.files;
184887
184887
  if (fil && fil.length > 0) {
184888
- const { path: path22, depth } = par;
184889
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path22));
184888
+ const { path: path23, depth } = par;
184889
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path23));
184890
184890
  const awaited = await Promise.all(slice);
184891
184891
  for (const entry of awaited) {
184892
184892
  if (!entry)
@@ -184926,20 +184926,20 @@ var ReaddirpStream = class extends Readable {
184926
184926
  this.reading = false;
184927
184927
  }
184928
184928
  }
184929
- async _exploreDir(path22, depth) {
184929
+ async _exploreDir(path23, depth) {
184930
184930
  let files;
184931
184931
  try {
184932
- files = await readdir(path22, this._rdOptions);
184932
+ files = await readdir(path23, this._rdOptions);
184933
184933
  } catch (error) {
184934
184934
  this._onError(error);
184935
184935
  }
184936
- return { files, depth, path: path22 };
184936
+ return { files, depth, path: path23 };
184937
184937
  }
184938
- async _formatEntry(dirent, path22) {
184938
+ async _formatEntry(dirent, path23) {
184939
184939
  let entry;
184940
184940
  const basename5 = this._isDirent ? dirent.name : dirent;
184941
184941
  try {
184942
- const fullPath = presolve(pjoin(path22, basename5));
184942
+ const fullPath = presolve(pjoin(path23, basename5));
184943
184943
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
184944
184944
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
184945
184945
  } catch (err) {
@@ -185339,16 +185339,16 @@ var delFromSet = (main, prop, item) => {
185339
185339
  };
185340
185340
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
185341
185341
  var FsWatchInstances = /* @__PURE__ */ new Map();
185342
- function createFsWatchInstance(path22, options2, listener, errHandler, emitRaw) {
185342
+ function createFsWatchInstance(path23, options2, listener, errHandler, emitRaw) {
185343
185343
  const handleEvent = (rawEvent, evPath) => {
185344
- listener(path22);
185345
- emitRaw(rawEvent, evPath, { watchedPath: path22 });
185346
- if (evPath && path22 !== evPath) {
185347
- fsWatchBroadcast(sp.resolve(path22, evPath), KEY_LISTENERS, sp.join(path22, evPath));
185344
+ listener(path23);
185345
+ emitRaw(rawEvent, evPath, { watchedPath: path23 });
185346
+ if (evPath && path23 !== evPath) {
185347
+ fsWatchBroadcast(sp.resolve(path23, evPath), KEY_LISTENERS, sp.join(path23, evPath));
185348
185348
  }
185349
185349
  };
185350
185350
  try {
185351
- return fs_watch(path22, {
185351
+ return fs_watch(path23, {
185352
185352
  persistent: options2.persistent
185353
185353
  }, handleEvent);
185354
185354
  } catch (error) {
@@ -185364,12 +185364,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
185364
185364
  listener(val1, val2, val3);
185365
185365
  });
185366
185366
  };
185367
- var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185367
+ var setFsWatchListener = (path23, fullPath, options2, handlers) => {
185368
185368
  const { listener, errHandler, rawEmitter } = handlers;
185369
185369
  let cont = FsWatchInstances.get(fullPath);
185370
185370
  let watcher;
185371
185371
  if (!options2.persistent) {
185372
- watcher = createFsWatchInstance(path22, options2, listener, errHandler, rawEmitter);
185372
+ watcher = createFsWatchInstance(path23, options2, listener, errHandler, rawEmitter);
185373
185373
  if (!watcher)
185374
185374
  return;
185375
185375
  return watcher.close.bind(watcher);
@@ -185380,7 +185380,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185380
185380
  addAndConvert(cont, KEY_RAW, rawEmitter);
185381
185381
  } else {
185382
185382
  watcher = createFsWatchInstance(
185383
- path22,
185383
+ path23,
185384
185384
  options2,
185385
185385
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
185386
185386
  errHandler,
@@ -185395,7 +185395,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185395
185395
  cont.watcherUnusable = true;
185396
185396
  if (isWindows && error.code === "EPERM") {
185397
185397
  try {
185398
- const fd = await open2(path22, "r");
185398
+ const fd = await open2(path23, "r");
185399
185399
  await fd.close();
185400
185400
  broadcastErr(error);
185401
185401
  } catch (err) {
@@ -185426,7 +185426,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185426
185426
  };
185427
185427
  };
185428
185428
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
185429
- var setFsWatchFileListener = (path22, fullPath, options2, handlers) => {
185429
+ var setFsWatchFileListener = (path23, fullPath, options2, handlers) => {
185430
185430
  const { listener, rawEmitter } = handlers;
185431
185431
  let cont = FsWatchFileInstances.get(fullPath);
185432
185432
  const copts = cont && cont.options;
@@ -185448,7 +185448,7 @@ var setFsWatchFileListener = (path22, fullPath, options2, handlers) => {
185448
185448
  });
185449
185449
  const currmtime = curr.mtimeMs;
185450
185450
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
185451
- foreach(cont.listeners, (listener2) => listener2(path22, curr));
185451
+ foreach(cont.listeners, (listener2) => listener2(path23, curr));
185452
185452
  }
185453
185453
  })
185454
185454
  };
@@ -185478,13 +185478,13 @@ var NodeFsHandler = class {
185478
185478
  * @param listener on fs change
185479
185479
  * @returns closer for the watcher instance
185480
185480
  */
185481
- _watchWithNodeFs(path22, listener) {
185481
+ _watchWithNodeFs(path23, listener) {
185482
185482
  const opts = this.fsw.options;
185483
- const directory = sp.dirname(path22);
185484
- const basename5 = sp.basename(path22);
185483
+ const directory = sp.dirname(path23);
185484
+ const basename5 = sp.basename(path23);
185485
185485
  const parent = this.fsw._getWatchedDir(directory);
185486
185486
  parent.add(basename5);
185487
- const absolutePath = sp.resolve(path22);
185487
+ const absolutePath = sp.resolve(path23);
185488
185488
  const options2 = {
185489
185489
  persistent: opts.persistent
185490
185490
  };
@@ -185494,12 +185494,12 @@ var NodeFsHandler = class {
185494
185494
  if (opts.usePolling) {
185495
185495
  const enableBin = opts.interval !== opts.binaryInterval;
185496
185496
  options2.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
185497
- closer = setFsWatchFileListener(path22, absolutePath, options2, {
185497
+ closer = setFsWatchFileListener(path23, absolutePath, options2, {
185498
185498
  listener,
185499
185499
  rawEmitter: this.fsw._emitRaw
185500
185500
  });
185501
185501
  } else {
185502
- closer = setFsWatchListener(path22, absolutePath, options2, {
185502
+ closer = setFsWatchListener(path23, absolutePath, options2, {
185503
185503
  listener,
185504
185504
  errHandler: this._boundHandleError,
185505
185505
  rawEmitter: this.fsw._emitRaw
@@ -185521,7 +185521,7 @@ var NodeFsHandler = class {
185521
185521
  let prevStats = stats;
185522
185522
  if (parent.has(basename5))
185523
185523
  return;
185524
- const listener = async (path22, newStats) => {
185524
+ const listener = async (path23, newStats) => {
185525
185525
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
185526
185526
  return;
185527
185527
  if (!newStats || newStats.mtimeMs === 0) {
@@ -185535,11 +185535,11 @@ var NodeFsHandler = class {
185535
185535
  this.fsw._emit(EV.CHANGE, file, newStats2);
185536
185536
  }
185537
185537
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
185538
- this.fsw._closeFile(path22);
185538
+ this.fsw._closeFile(path23);
185539
185539
  prevStats = newStats2;
185540
185540
  const closer2 = this._watchWithNodeFs(file, listener);
185541
185541
  if (closer2)
185542
- this.fsw._addPathCloser(path22, closer2);
185542
+ this.fsw._addPathCloser(path23, closer2);
185543
185543
  } else {
185544
185544
  prevStats = newStats2;
185545
185545
  }
@@ -185571,7 +185571,7 @@ var NodeFsHandler = class {
185571
185571
  * @param item basename of this item
185572
185572
  * @returns true if no more processing is needed for this entry.
185573
185573
  */
185574
- async _handleSymlink(entry, directory, path22, item) {
185574
+ async _handleSymlink(entry, directory, path23, item) {
185575
185575
  if (this.fsw.closed) {
185576
185576
  return;
185577
185577
  }
@@ -185581,7 +185581,7 @@ var NodeFsHandler = class {
185581
185581
  this.fsw._incrReadyCount();
185582
185582
  let linkPath;
185583
185583
  try {
185584
- linkPath = await fsrealpath(path22);
185584
+ linkPath = await fsrealpath(path23);
185585
185585
  } catch (e) {
185586
185586
  this.fsw._emitReady();
185587
185587
  return true;
@@ -185591,12 +185591,12 @@ var NodeFsHandler = class {
185591
185591
  if (dir.has(item)) {
185592
185592
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
185593
185593
  this.fsw._symlinkPaths.set(full, linkPath);
185594
- this.fsw._emit(EV.CHANGE, path22, entry.stats);
185594
+ this.fsw._emit(EV.CHANGE, path23, entry.stats);
185595
185595
  }
185596
185596
  } else {
185597
185597
  dir.add(item);
185598
185598
  this.fsw._symlinkPaths.set(full, linkPath);
185599
- this.fsw._emit(EV.ADD, path22, entry.stats);
185599
+ this.fsw._emit(EV.ADD, path23, entry.stats);
185600
185600
  }
185601
185601
  this.fsw._emitReady();
185602
185602
  return true;
@@ -185626,9 +185626,9 @@ var NodeFsHandler = class {
185626
185626
  return;
185627
185627
  }
185628
185628
  const item = entry.path;
185629
- let path22 = sp.join(directory, item);
185629
+ let path23 = sp.join(directory, item);
185630
185630
  current.add(item);
185631
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path22, item)) {
185631
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path23, item)) {
185632
185632
  return;
185633
185633
  }
185634
185634
  if (this.fsw.closed) {
@@ -185637,8 +185637,8 @@ var NodeFsHandler = class {
185637
185637
  }
185638
185638
  if (item === target || !target && !previous.has(item)) {
185639
185639
  this.fsw._incrReadyCount();
185640
- path22 = sp.join(dir, sp.relative(dir, path22));
185641
- this._addToNodeFs(path22, initialAdd, wh, depth + 1);
185640
+ path23 = sp.join(dir, sp.relative(dir, path23));
185641
+ this._addToNodeFs(path23, initialAdd, wh, depth + 1);
185642
185642
  }
185643
185643
  }).on(EV.ERROR, this._boundHandleError);
185644
185644
  return new Promise((resolve7, reject) => {
@@ -185707,13 +185707,13 @@ var NodeFsHandler = class {
185707
185707
  * @param depth Child path actually targeted for watch
185708
185708
  * @param target Child path actually targeted for watch
185709
185709
  */
185710
- async _addToNodeFs(path22, initialAdd, priorWh, depth, target) {
185710
+ async _addToNodeFs(path23, initialAdd, priorWh, depth, target) {
185711
185711
  const ready = this.fsw._emitReady;
185712
- if (this.fsw._isIgnored(path22) || this.fsw.closed) {
185712
+ if (this.fsw._isIgnored(path23) || this.fsw.closed) {
185713
185713
  ready();
185714
185714
  return false;
185715
185715
  }
185716
- const wh = this.fsw._getWatchHelpers(path22);
185716
+ const wh = this.fsw._getWatchHelpers(path23);
185717
185717
  if (priorWh) {
185718
185718
  wh.filterPath = (entry) => priorWh.filterPath(entry);
185719
185719
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -185729,8 +185729,8 @@ var NodeFsHandler = class {
185729
185729
  const follow = this.fsw.options.followSymlinks;
185730
185730
  let closer;
185731
185731
  if (stats.isDirectory()) {
185732
- const absPath = sp.resolve(path22);
185733
- const targetPath = follow ? await fsrealpath(path22) : path22;
185732
+ const absPath = sp.resolve(path23);
185733
+ const targetPath = follow ? await fsrealpath(path23) : path23;
185734
185734
  if (this.fsw.closed)
185735
185735
  return;
185736
185736
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -185740,29 +185740,29 @@ var NodeFsHandler = class {
185740
185740
  this.fsw._symlinkPaths.set(absPath, targetPath);
185741
185741
  }
185742
185742
  } else if (stats.isSymbolicLink()) {
185743
- const targetPath = follow ? await fsrealpath(path22) : path22;
185743
+ const targetPath = follow ? await fsrealpath(path23) : path23;
185744
185744
  if (this.fsw.closed)
185745
185745
  return;
185746
185746
  const parent = sp.dirname(wh.watchPath);
185747
185747
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
185748
185748
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
185749
- closer = await this._handleDir(parent, stats, initialAdd, depth, path22, wh, targetPath);
185749
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path23, wh, targetPath);
185750
185750
  if (this.fsw.closed)
185751
185751
  return;
185752
185752
  if (targetPath !== void 0) {
185753
- this.fsw._symlinkPaths.set(sp.resolve(path22), targetPath);
185753
+ this.fsw._symlinkPaths.set(sp.resolve(path23), targetPath);
185754
185754
  }
185755
185755
  } else {
185756
185756
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
185757
185757
  }
185758
185758
  ready();
185759
185759
  if (closer)
185760
- this.fsw._addPathCloser(path22, closer);
185760
+ this.fsw._addPathCloser(path23, closer);
185761
185761
  return false;
185762
185762
  } catch (error) {
185763
185763
  if (this.fsw._handleError(error)) {
185764
185764
  ready();
185765
- return path22;
185765
+ return path23;
185766
185766
  }
185767
185767
  }
185768
185768
  }
@@ -185805,24 +185805,24 @@ function createPattern(matcher) {
185805
185805
  }
185806
185806
  return () => false;
185807
185807
  }
185808
- function normalizePath(path22) {
185809
- if (typeof path22 !== "string")
185808
+ function normalizePath(path23) {
185809
+ if (typeof path23 !== "string")
185810
185810
  throw new Error("string expected");
185811
- path22 = sp2.normalize(path22);
185812
- path22 = path22.replace(/\\/g, "/");
185811
+ path23 = sp2.normalize(path23);
185812
+ path23 = path23.replace(/\\/g, "/");
185813
185813
  let prepend = false;
185814
- if (path22.startsWith("//"))
185814
+ if (path23.startsWith("//"))
185815
185815
  prepend = true;
185816
- path22 = path22.replace(DOUBLE_SLASH_RE, "/");
185816
+ path23 = path23.replace(DOUBLE_SLASH_RE, "/");
185817
185817
  if (prepend)
185818
- path22 = "/" + path22;
185819
- return path22;
185818
+ path23 = "/" + path23;
185819
+ return path23;
185820
185820
  }
185821
185821
  function matchPatterns(patterns, testString, stats) {
185822
- const path22 = normalizePath(testString);
185822
+ const path23 = normalizePath(testString);
185823
185823
  for (let index = 0; index < patterns.length; index++) {
185824
185824
  const pattern = patterns[index];
185825
- if (pattern(path22, stats)) {
185825
+ if (pattern(path23, stats)) {
185826
185826
  return true;
185827
185827
  }
185828
185828
  }
@@ -185860,19 +185860,19 @@ var toUnix = (string) => {
185860
185860
  }
185861
185861
  return str;
185862
185862
  };
185863
- var normalizePathToUnix = (path22) => toUnix(sp2.normalize(toUnix(path22)));
185864
- var normalizeIgnored = (cwd = "") => (path22) => {
185865
- if (typeof path22 === "string") {
185866
- return normalizePathToUnix(sp2.isAbsolute(path22) ? path22 : sp2.join(cwd, path22));
185863
+ var normalizePathToUnix = (path23) => toUnix(sp2.normalize(toUnix(path23)));
185864
+ var normalizeIgnored = (cwd = "") => (path23) => {
185865
+ if (typeof path23 === "string") {
185866
+ return normalizePathToUnix(sp2.isAbsolute(path23) ? path23 : sp2.join(cwd, path23));
185867
185867
  } else {
185868
- return path22;
185868
+ return path23;
185869
185869
  }
185870
185870
  };
185871
- var getAbsolutePath = (path22, cwd) => {
185872
- if (sp2.isAbsolute(path22)) {
185873
- return path22;
185871
+ var getAbsolutePath = (path23, cwd) => {
185872
+ if (sp2.isAbsolute(path23)) {
185873
+ return path23;
185874
185874
  }
185875
- return sp2.join(cwd, path22);
185875
+ return sp2.join(cwd, path23);
185876
185876
  };
185877
185877
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
185878
185878
  var DirEntry = class {
@@ -185937,10 +185937,10 @@ var WatchHelper = class {
185937
185937
  dirParts;
185938
185938
  followSymlinks;
185939
185939
  statMethod;
185940
- constructor(path22, follow, fsw) {
185940
+ constructor(path23, follow, fsw) {
185941
185941
  this.fsw = fsw;
185942
- const watchPath = path22;
185943
- this.path = path22 = path22.replace(REPLACER_RE, "");
185942
+ const watchPath = path23;
185943
+ this.path = path23 = path23.replace(REPLACER_RE, "");
185944
185944
  this.watchPath = watchPath;
185945
185945
  this.fullWatchPath = sp2.resolve(watchPath);
185946
185946
  this.dirParts = [];
@@ -186080,20 +186080,20 @@ var FSWatcher = class extends EventEmitter {
186080
186080
  this._closePromise = void 0;
186081
186081
  let paths = unifyPaths(paths_);
186082
186082
  if (cwd) {
186083
- paths = paths.map((path22) => {
186084
- const absPath = getAbsolutePath(path22, cwd);
186083
+ paths = paths.map((path23) => {
186084
+ const absPath = getAbsolutePath(path23, cwd);
186085
186085
  return absPath;
186086
186086
  });
186087
186087
  }
186088
- paths.forEach((path22) => {
186089
- this._removeIgnoredPath(path22);
186088
+ paths.forEach((path23) => {
186089
+ this._removeIgnoredPath(path23);
186090
186090
  });
186091
186091
  this._userIgnored = void 0;
186092
186092
  if (!this._readyCount)
186093
186093
  this._readyCount = 0;
186094
186094
  this._readyCount += paths.length;
186095
- Promise.all(paths.map(async (path22) => {
186096
- const res = await this._nodeFsHandler._addToNodeFs(path22, !_internal, void 0, 0, _origAdd);
186095
+ Promise.all(paths.map(async (path23) => {
186096
+ const res = await this._nodeFsHandler._addToNodeFs(path23, !_internal, void 0, 0, _origAdd);
186097
186097
  if (res)
186098
186098
  this._emitReady();
186099
186099
  return res;
@@ -186115,17 +186115,17 @@ var FSWatcher = class extends EventEmitter {
186115
186115
  return this;
186116
186116
  const paths = unifyPaths(paths_);
186117
186117
  const { cwd } = this.options;
186118
- paths.forEach((path22) => {
186119
- if (!sp2.isAbsolute(path22) && !this._closers.has(path22)) {
186118
+ paths.forEach((path23) => {
186119
+ if (!sp2.isAbsolute(path23) && !this._closers.has(path23)) {
186120
186120
  if (cwd)
186121
- path22 = sp2.join(cwd, path22);
186122
- path22 = sp2.resolve(path22);
186121
+ path23 = sp2.join(cwd, path23);
186122
+ path23 = sp2.resolve(path23);
186123
186123
  }
186124
- this._closePath(path22);
186125
- this._addIgnoredPath(path22);
186126
- if (this._watched.has(path22)) {
186124
+ this._closePath(path23);
186125
+ this._addIgnoredPath(path23);
186126
+ if (this._watched.has(path23)) {
186127
186127
  this._addIgnoredPath({
186128
- path: path22,
186128
+ path: path23,
186129
186129
  recursive: true
186130
186130
  });
186131
186131
  }
@@ -186189,38 +186189,38 @@ var FSWatcher = class extends EventEmitter {
186189
186189
  * @param stats arguments to be passed with event
186190
186190
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
186191
186191
  */
186192
- async _emit(event, path22, stats) {
186192
+ async _emit(event, path23, stats) {
186193
186193
  if (this.closed)
186194
186194
  return;
186195
186195
  const opts = this.options;
186196
186196
  if (isWindows)
186197
- path22 = sp2.normalize(path22);
186197
+ path23 = sp2.normalize(path23);
186198
186198
  if (opts.cwd)
186199
- path22 = sp2.relative(opts.cwd, path22);
186200
- const args = [path22];
186199
+ path23 = sp2.relative(opts.cwd, path23);
186200
+ const args = [path23];
186201
186201
  if (stats != null)
186202
186202
  args.push(stats);
186203
186203
  const awf = opts.awaitWriteFinish;
186204
186204
  let pw;
186205
- if (awf && (pw = this._pendingWrites.get(path22))) {
186205
+ if (awf && (pw = this._pendingWrites.get(path23))) {
186206
186206
  pw.lastChange = /* @__PURE__ */ new Date();
186207
186207
  return this;
186208
186208
  }
186209
186209
  if (opts.atomic) {
186210
186210
  if (event === EVENTS.UNLINK) {
186211
- this._pendingUnlinks.set(path22, [event, ...args]);
186211
+ this._pendingUnlinks.set(path23, [event, ...args]);
186212
186212
  setTimeout(() => {
186213
- this._pendingUnlinks.forEach((entry, path23) => {
186213
+ this._pendingUnlinks.forEach((entry, path24) => {
186214
186214
  this.emit(...entry);
186215
186215
  this.emit(EVENTS.ALL, ...entry);
186216
- this._pendingUnlinks.delete(path23);
186216
+ this._pendingUnlinks.delete(path24);
186217
186217
  });
186218
186218
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
186219
186219
  return this;
186220
186220
  }
186221
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path22)) {
186221
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path23)) {
186222
186222
  event = EVENTS.CHANGE;
186223
- this._pendingUnlinks.delete(path22);
186223
+ this._pendingUnlinks.delete(path23);
186224
186224
  }
186225
186225
  }
186226
186226
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -186238,16 +186238,16 @@ var FSWatcher = class extends EventEmitter {
186238
186238
  this.emitWithAll(event, args);
186239
186239
  }
186240
186240
  };
186241
- this._awaitWriteFinish(path22, awf.stabilityThreshold, event, awfEmit);
186241
+ this._awaitWriteFinish(path23, awf.stabilityThreshold, event, awfEmit);
186242
186242
  return this;
186243
186243
  }
186244
186244
  if (event === EVENTS.CHANGE) {
186245
- const isThrottled = !this._throttle(EVENTS.CHANGE, path22, 50);
186245
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path23, 50);
186246
186246
  if (isThrottled)
186247
186247
  return this;
186248
186248
  }
186249
186249
  if (opts.alwaysStat && stats === void 0 && (event === EVENTS.ADD || event === EVENTS.ADD_DIR || event === EVENTS.CHANGE)) {
186250
- const fullPath = opts.cwd ? sp2.join(opts.cwd, path22) : path22;
186250
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path23) : path23;
186251
186251
  let stats2;
186252
186252
  try {
186253
186253
  stats2 = await stat3(fullPath);
@@ -186278,23 +186278,23 @@ var FSWatcher = class extends EventEmitter {
186278
186278
  * @param timeout duration of time to suppress duplicate actions
186279
186279
  * @returns tracking object or false if action should be suppressed
186280
186280
  */
186281
- _throttle(actionType, path22, timeout) {
186281
+ _throttle(actionType, path23, timeout) {
186282
186282
  if (!this._throttled.has(actionType)) {
186283
186283
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
186284
186284
  }
186285
186285
  const action = this._throttled.get(actionType);
186286
186286
  if (!action)
186287
186287
  throw new Error("invalid throttle");
186288
- const actionPath = action.get(path22);
186288
+ const actionPath = action.get(path23);
186289
186289
  if (actionPath) {
186290
186290
  actionPath.count++;
186291
186291
  return false;
186292
186292
  }
186293
186293
  let timeoutObject;
186294
186294
  const clear = () => {
186295
- const item = action.get(path22);
186295
+ const item = action.get(path23);
186296
186296
  const count = item ? item.count : 0;
186297
- action.delete(path22);
186297
+ action.delete(path23);
186298
186298
  clearTimeout(timeoutObject);
186299
186299
  if (item)
186300
186300
  clearTimeout(item.timeoutObject);
@@ -186302,7 +186302,7 @@ var FSWatcher = class extends EventEmitter {
186302
186302
  };
186303
186303
  timeoutObject = setTimeout(clear, timeout);
186304
186304
  const thr = { timeoutObject, clear, count: 0 };
186305
- action.set(path22, thr);
186305
+ action.set(path23, thr);
186306
186306
  return thr;
186307
186307
  }
186308
186308
  _incrReadyCount() {
@@ -186316,44 +186316,44 @@ var FSWatcher = class extends EventEmitter {
186316
186316
  * @param event
186317
186317
  * @param awfEmit Callback to be called when ready for event to be emitted.
186318
186318
  */
186319
- _awaitWriteFinish(path22, threshold, event, awfEmit) {
186319
+ _awaitWriteFinish(path23, threshold, event, awfEmit) {
186320
186320
  const awf = this.options.awaitWriteFinish;
186321
186321
  if (typeof awf !== "object")
186322
186322
  return;
186323
186323
  const pollInterval = awf.pollInterval;
186324
186324
  let timeoutHandler;
186325
- let fullPath = path22;
186326
- if (this.options.cwd && !sp2.isAbsolute(path22)) {
186327
- fullPath = sp2.join(this.options.cwd, path22);
186325
+ let fullPath = path23;
186326
+ if (this.options.cwd && !sp2.isAbsolute(path23)) {
186327
+ fullPath = sp2.join(this.options.cwd, path23);
186328
186328
  }
186329
186329
  const now = /* @__PURE__ */ new Date();
186330
186330
  const writes = this._pendingWrites;
186331
186331
  function awaitWriteFinishFn(prevStat) {
186332
186332
  statcb(fullPath, (err, curStat) => {
186333
- if (err || !writes.has(path22)) {
186333
+ if (err || !writes.has(path23)) {
186334
186334
  if (err && err.code !== "ENOENT")
186335
186335
  awfEmit(err);
186336
186336
  return;
186337
186337
  }
186338
186338
  const now2 = Number(/* @__PURE__ */ new Date());
186339
186339
  if (prevStat && curStat.size !== prevStat.size) {
186340
- writes.get(path22).lastChange = now2;
186340
+ writes.get(path23).lastChange = now2;
186341
186341
  }
186342
- const pw = writes.get(path22);
186342
+ const pw = writes.get(path23);
186343
186343
  const df = now2 - pw.lastChange;
186344
186344
  if (df >= threshold) {
186345
- writes.delete(path22);
186345
+ writes.delete(path23);
186346
186346
  awfEmit(void 0, curStat);
186347
186347
  } else {
186348
186348
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
186349
186349
  }
186350
186350
  });
186351
186351
  }
186352
- if (!writes.has(path22)) {
186353
- writes.set(path22, {
186352
+ if (!writes.has(path23)) {
186353
+ writes.set(path23, {
186354
186354
  lastChange: now,
186355
186355
  cancelWait: () => {
186356
- writes.delete(path22);
186356
+ writes.delete(path23);
186357
186357
  clearTimeout(timeoutHandler);
186358
186358
  return event;
186359
186359
  }
@@ -186364,8 +186364,8 @@ var FSWatcher = class extends EventEmitter {
186364
186364
  /**
186365
186365
  * Determines whether user has asked to ignore this path.
186366
186366
  */
186367
- _isIgnored(path22, stats) {
186368
- if (this.options.atomic && DOT_RE.test(path22))
186367
+ _isIgnored(path23, stats) {
186368
+ if (this.options.atomic && DOT_RE.test(path23))
186369
186369
  return true;
186370
186370
  if (!this._userIgnored) {
186371
186371
  const { cwd } = this.options;
@@ -186375,17 +186375,17 @@ var FSWatcher = class extends EventEmitter {
186375
186375
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
186376
186376
  this._userIgnored = anymatch(list, void 0);
186377
186377
  }
186378
- return this._userIgnored(path22, stats);
186378
+ return this._userIgnored(path23, stats);
186379
186379
  }
186380
- _isntIgnored(path22, stat4) {
186381
- return !this._isIgnored(path22, stat4);
186380
+ _isntIgnored(path23, stat4) {
186381
+ return !this._isIgnored(path23, stat4);
186382
186382
  }
186383
186383
  /**
186384
186384
  * Provides a set of common helpers and properties relating to symlink handling.
186385
186385
  * @param path file or directory pattern being watched
186386
186386
  */
186387
- _getWatchHelpers(path22) {
186388
- return new WatchHelper(path22, this.options.followSymlinks, this);
186387
+ _getWatchHelpers(path23) {
186388
+ return new WatchHelper(path23, this.options.followSymlinks, this);
186389
186389
  }
186390
186390
  // Directory helpers
186391
186391
  // -----------------
@@ -186417,63 +186417,63 @@ var FSWatcher = class extends EventEmitter {
186417
186417
  * @param item base path of item/directory
186418
186418
  */
186419
186419
  _remove(directory, item, isDirectory) {
186420
- const path22 = sp2.join(directory, item);
186421
- const fullPath = sp2.resolve(path22);
186422
- isDirectory = isDirectory != null ? isDirectory : this._watched.has(path22) || this._watched.has(fullPath);
186423
- if (!this._throttle("remove", path22, 100))
186420
+ const path23 = sp2.join(directory, item);
186421
+ const fullPath = sp2.resolve(path23);
186422
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path23) || this._watched.has(fullPath);
186423
+ if (!this._throttle("remove", path23, 100))
186424
186424
  return;
186425
186425
  if (!isDirectory && this._watched.size === 1) {
186426
186426
  this.add(directory, item, true);
186427
186427
  }
186428
- const wp = this._getWatchedDir(path22);
186428
+ const wp = this._getWatchedDir(path23);
186429
186429
  const nestedDirectoryChildren = wp.getChildren();
186430
- nestedDirectoryChildren.forEach((nested) => this._remove(path22, nested));
186430
+ nestedDirectoryChildren.forEach((nested) => this._remove(path23, nested));
186431
186431
  const parent = this._getWatchedDir(directory);
186432
186432
  const wasTracked = parent.has(item);
186433
186433
  parent.remove(item);
186434
186434
  if (this._symlinkPaths.has(fullPath)) {
186435
186435
  this._symlinkPaths.delete(fullPath);
186436
186436
  }
186437
- let relPath = path22;
186437
+ let relPath = path23;
186438
186438
  if (this.options.cwd)
186439
- relPath = sp2.relative(this.options.cwd, path22);
186439
+ relPath = sp2.relative(this.options.cwd, path23);
186440
186440
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
186441
186441
  const event = this._pendingWrites.get(relPath).cancelWait();
186442
186442
  if (event === EVENTS.ADD)
186443
186443
  return;
186444
186444
  }
186445
- this._watched.delete(path22);
186445
+ this._watched.delete(path23);
186446
186446
  this._watched.delete(fullPath);
186447
186447
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
186448
- if (wasTracked && !this._isIgnored(path22))
186449
- this._emit(eventName, path22);
186450
- this._closePath(path22);
186448
+ if (wasTracked && !this._isIgnored(path23))
186449
+ this._emit(eventName, path23);
186450
+ this._closePath(path23);
186451
186451
  }
186452
186452
  /**
186453
186453
  * Closes all watchers for a path
186454
186454
  */
186455
- _closePath(path22) {
186456
- this._closeFile(path22);
186457
- const dir = sp2.dirname(path22);
186458
- this._getWatchedDir(dir).remove(sp2.basename(path22));
186455
+ _closePath(path23) {
186456
+ this._closeFile(path23);
186457
+ const dir = sp2.dirname(path23);
186458
+ this._getWatchedDir(dir).remove(sp2.basename(path23));
186459
186459
  }
186460
186460
  /**
186461
186461
  * Closes only file-specific watchers
186462
186462
  */
186463
- _closeFile(path22) {
186464
- const closers = this._closers.get(path22);
186463
+ _closeFile(path23) {
186464
+ const closers = this._closers.get(path23);
186465
186465
  if (!closers)
186466
186466
  return;
186467
186467
  closers.forEach((closer) => closer());
186468
- this._closers.delete(path22);
186468
+ this._closers.delete(path23);
186469
186469
  }
186470
- _addPathCloser(path22, closer) {
186470
+ _addPathCloser(path23, closer) {
186471
186471
  if (!closer)
186472
186472
  return;
186473
- let list = this._closers.get(path22);
186473
+ let list = this._closers.get(path23);
186474
186474
  if (!list) {
186475
186475
  list = [];
186476
- this._closers.set(path22, list);
186476
+ this._closers.set(path23, list);
186477
186477
  }
186478
186478
  list.push(closer);
186479
186479
  }
@@ -188620,10 +188620,108 @@ function watchConfigFile(configPath, debounceMs, onChange) {
188620
188620
  };
188621
188621
  }
188622
188622
 
188623
- // src/lib/dev/proxy-registry.ts
188623
+ // src/lib/dev/subdomain-generator.ts
188624
188624
  import * as fs18 from "fs";
188625
188625
  import * as path15 from "path";
188626
+ import { generateSlug } from "random-word-slugs";
188627
+ var StableSubdomainAllocator = class {
188628
+ tunnelsDir;
188629
+ tunnelsFilePath;
188630
+ baseSlug = null;
188631
+ constructor(projectRoot, key = "default") {
188632
+ this.tunnelsDir = path15.join(projectRoot, ".specific", "keys", key);
188633
+ this.tunnelsFilePath = path15.join(this.tunnelsDir, "tunnels.json");
188634
+ this.loadTunnels();
188635
+ }
188636
+ loadTunnels() {
188637
+ if (!fs18.existsSync(this.tunnelsFilePath)) {
188638
+ return;
188639
+ }
188640
+ try {
188641
+ const content = fs18.readFileSync(this.tunnelsFilePath, "utf-8");
188642
+ const data = JSON.parse(content);
188643
+ if (data.version === 1 && data.baseSlug) {
188644
+ this.baseSlug = data.baseSlug;
188645
+ }
188646
+ } catch {
188647
+ this.baseSlug = null;
188648
+ }
188649
+ }
188650
+ saveTunnels() {
188651
+ if (!fs18.existsSync(this.tunnelsDir)) {
188652
+ fs18.mkdirSync(this.tunnelsDir, { recursive: true });
188653
+ }
188654
+ const data = {
188655
+ version: 1,
188656
+ baseSlug: this.baseSlug
188657
+ };
188658
+ fs18.writeFileSync(this.tunnelsFilePath, JSON.stringify(data, null, 2));
188659
+ }
188660
+ generateBaseSlug() {
188661
+ return generateSlug(2, {
188662
+ format: "kebab",
188663
+ partsOfSpeech: ["adjective", "noun"],
188664
+ categories: {
188665
+ adjective: ["color", "appearance"],
188666
+ noun: ["animals"]
188667
+ }
188668
+ });
188669
+ }
188670
+ /**
188671
+ * Get the base slug, generating one if needed.
188672
+ */
188673
+ getBaseSlug() {
188674
+ if (!this.baseSlug) {
188675
+ this.baseSlug = this.generateBaseSlug();
188676
+ this.saveTunnels();
188677
+ }
188678
+ return this.baseSlug;
188679
+ }
188680
+ /**
188681
+ * Allocate a subdomain for a service.
188682
+ * If multipleServices is true, appends the service name to the base slug.
188683
+ */
188684
+ allocate(serviceName, multipleServices) {
188685
+ const baseSlug = this.getBaseSlug();
188686
+ if (multipleServices) {
188687
+ return `${baseSlug}-${serviceName}`;
188688
+ }
188689
+ return baseSlug;
188690
+ }
188691
+ };
188692
+
188693
+ // src/lib/dev/tunnel-manager.ts
188694
+ import localtunnel from "localtunnel";
188695
+ var TUNNEL_HOST = "https://tunnel.spcf.app";
188696
+ async function startTunnel(serviceName, endpointName, port, subdomain, callbacks) {
188697
+ const tunnel = await localtunnel({
188698
+ port,
188699
+ subdomain,
188700
+ host: TUNNEL_HOST
188701
+ });
188702
+ tunnel.on("error", (err) => {
188703
+ callbacks?.onError?.(serviceName, endpointName, err);
188704
+ });
188705
+ tunnel.on("close", () => {
188706
+ callbacks?.onClose?.(serviceName, endpointName);
188707
+ });
188708
+ return {
188709
+ serviceName,
188710
+ endpointName,
188711
+ localPort: port,
188712
+ url: tunnel.url,
188713
+ subdomain,
188714
+ stop: async () => {
188715
+ tunnel.close();
188716
+ }
188717
+ };
188718
+ }
188719
+
188720
+ // src/lib/dev/proxy-registry.ts
188721
+ import * as fs19 from "fs";
188722
+ import * as path16 from "path";
188626
188723
  import * as os8 from "os";
188724
+ import * as net4 from "net";
188627
188725
  var ProxyRegistryManager = class {
188628
188726
  proxyDir;
188629
188727
  ownerPath;
@@ -188632,14 +188730,14 @@ var ProxyRegistryManager = class {
188632
188730
  isOwner = false;
188633
188731
  registryWatcher = null;
188634
188732
  constructor() {
188635
- this.proxyDir = path15.join(os8.homedir(), ".specific", "proxy");
188636
- this.ownerPath = path15.join(this.proxyDir, "owner.json");
188637
- this.registryPath = path15.join(this.proxyDir, "registry.json");
188638
- this.lockPath = path15.join(this.proxyDir, "registry.lock");
188733
+ this.proxyDir = path16.join(os8.homedir(), ".specific", "proxy");
188734
+ this.ownerPath = path16.join(this.proxyDir, "owner.json");
188735
+ this.registryPath = path16.join(this.proxyDir, "registry.json");
188736
+ this.lockPath = path16.join(this.proxyDir, "registry.lock");
188639
188737
  }
188640
188738
  ensureProxyDir() {
188641
- if (!fs18.existsSync(this.proxyDir)) {
188642
- fs18.mkdirSync(this.proxyDir, { recursive: true });
188739
+ if (!fs19.existsSync(this.proxyDir)) {
188740
+ fs19.mkdirSync(this.proxyDir, { recursive: true });
188643
188741
  }
188644
188742
  }
188645
188743
  isProcessRunning(pid) {
@@ -188651,20 +188749,60 @@ var ProxyRegistryManager = class {
188651
188749
  return err.code !== "ESRCH";
188652
188750
  }
188653
188751
  }
188752
+ /**
188753
+ * Check if the proxy is actually listening on its expected port.
188754
+ * This catches cases where the owner process is alive but the proxy has crashed.
188755
+ */
188756
+ isProxyListening(port, timeoutMs = 1e3) {
188757
+ return new Promise((resolve7) => {
188758
+ const socket = new net4.Socket();
188759
+ let resolved = false;
188760
+ const cleanup = () => {
188761
+ if (!resolved) {
188762
+ resolved = true;
188763
+ socket.destroy();
188764
+ }
188765
+ };
188766
+ socket.setTimeout(timeoutMs);
188767
+ socket.on("connect", () => {
188768
+ cleanup();
188769
+ resolve7(true);
188770
+ });
188771
+ socket.on("timeout", () => {
188772
+ cleanup();
188773
+ resolve7(false);
188774
+ });
188775
+ socket.on("error", () => {
188776
+ cleanup();
188777
+ resolve7(false);
188778
+ });
188779
+ socket.connect(port, "127.0.0.1");
188780
+ });
188781
+ }
188782
+ /**
188783
+ * Check if the proxy owner is healthy (process running AND proxy listening).
188784
+ */
188785
+ async isProxyOwnerHealthy(pid) {
188786
+ if (!this.isProcessRunning(pid)) {
188787
+ return false;
188788
+ }
188789
+ const isListening = await this.isProxyListening(443);
188790
+ return isListening;
188791
+ }
188654
188792
  async acquireLock(timeoutMs = 5e3) {
188655
188793
  this.ensureProxyDir();
188656
188794
  const startTime = Date.now();
188657
188795
  while (Date.now() - startTime < timeoutMs) {
188658
188796
  try {
188659
- const fd = fs18.openSync(
188797
+ const fd = fs19.openSync(
188660
188798
  this.lockPath,
188661
- fs18.constants.O_CREAT | fs18.constants.O_EXCL | fs18.constants.O_WRONLY
188799
+ fs19.constants.O_CREAT | fs19.constants.O_EXCL | fs19.constants.O_WRONLY
188662
188800
  );
188663
- fs18.writeSync(fd, String(process.pid));
188664
- fs18.closeSync(fd);
188801
+ fs19.writeSync(fd, String(process.pid));
188802
+ fs19.closeSync(fd);
188665
188803
  return () => {
188666
188804
  try {
188667
- fs18.unlinkSync(this.lockPath);
188805
+ fs19.unlinkSync(this.lockPath);
188668
188806
  } catch {
188669
188807
  }
188670
188808
  };
@@ -188673,16 +188811,16 @@ var ProxyRegistryManager = class {
188673
188811
  if (err.code === "EEXIST") {
188674
188812
  try {
188675
188813
  const lockPid = parseInt(
188676
- fs18.readFileSync(this.lockPath, "utf-8").trim(),
188814
+ fs19.readFileSync(this.lockPath, "utf-8").trim(),
188677
188815
  10
188678
188816
  );
188679
188817
  if (!this.isProcessRunning(lockPid)) {
188680
- fs18.unlinkSync(this.lockPath);
188818
+ fs19.unlinkSync(this.lockPath);
188681
188819
  continue;
188682
188820
  }
188683
188821
  } catch {
188684
188822
  try {
188685
- fs18.unlinkSync(this.lockPath);
188823
+ fs19.unlinkSync(this.lockPath);
188686
188824
  } catch {
188687
188825
  }
188688
188826
  continue;
@@ -188702,10 +188840,10 @@ var ProxyRegistryManager = class {
188702
188840
  async claimProxyOwnership(key) {
188703
188841
  const releaseLock = await this.acquireLock();
188704
188842
  try {
188705
- if (fs18.existsSync(this.ownerPath)) {
188706
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188843
+ if (fs19.existsSync(this.ownerPath)) {
188844
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188707
188845
  const ownerFile2 = JSON.parse(content);
188708
- if (this.isProcessRunning(ownerFile2.owner.pid)) {
188846
+ if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188709
188847
  return false;
188710
188848
  }
188711
188849
  }
@@ -188733,11 +188871,11 @@ var ProxyRegistryManager = class {
188733
188871
  }
188734
188872
  const releaseLock = await this.acquireLock();
188735
188873
  try {
188736
- if (fs18.existsSync(this.ownerPath)) {
188737
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188874
+ if (fs19.existsSync(this.ownerPath)) {
188875
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188738
188876
  const ownerFile = JSON.parse(content);
188739
188877
  if (ownerFile.owner.pid === process.pid) {
188740
- fs18.unlinkSync(this.ownerPath);
188878
+ fs19.unlinkSync(this.ownerPath);
188741
188879
  }
188742
188880
  }
188743
188881
  this.isOwner = false;
@@ -188749,14 +188887,14 @@ var ProxyRegistryManager = class {
188749
188887
  * Get the current proxy owner.
188750
188888
  */
188751
188889
  async getProxyOwner() {
188752
- if (!fs18.existsSync(this.ownerPath)) {
188890
+ if (!fs19.existsSync(this.ownerPath)) {
188753
188891
  return null;
188754
188892
  }
188755
188893
  const releaseLock = await this.acquireLock();
188756
188894
  try {
188757
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188895
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188758
188896
  const ownerFile = JSON.parse(content);
188759
- if (!this.isProcessRunning(ownerFile.owner.pid)) {
188897
+ if (!await this.isProxyOwnerHealthy(ownerFile.owner.pid)) {
188760
188898
  return null;
188761
188899
  }
188762
188900
  return ownerFile.owner;
@@ -188848,7 +188986,7 @@ var ProxyRegistryManager = class {
188848
188986
  */
188849
188987
  watchRegistry(onChange) {
188850
188988
  this.ensureProxyDir();
188851
- if (!fs18.existsSync(this.registryPath)) {
188989
+ if (!fs19.existsSync(this.registryPath)) {
188852
188990
  const emptyRegistry = {
188853
188991
  version: 1,
188854
188992
  keys: {},
@@ -188892,13 +189030,13 @@ var ProxyRegistryManager = class {
188892
189030
  async attemptElection(key) {
188893
189031
  const releaseLock = await this.acquireLock();
188894
189032
  try {
188895
- if (fs18.existsSync(this.ownerPath)) {
188896
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
189033
+ if (fs19.existsSync(this.ownerPath)) {
189034
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188897
189035
  const ownerFile2 = JSON.parse(content);
188898
- if (this.isProcessRunning(ownerFile2.owner.pid)) {
189036
+ if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188899
189037
  return false;
188900
189038
  }
188901
- fs18.unlinkSync(this.ownerPath);
189039
+ fs19.unlinkSync(this.ownerPath);
188902
189040
  }
188903
189041
  const ownerFile = {
188904
189042
  version: 1,
@@ -188916,7 +189054,7 @@ var ProxyRegistryManager = class {
188916
189054
  }
188917
189055
  }
188918
189056
  readRegistry() {
188919
- if (!fs18.existsSync(this.registryPath)) {
189057
+ if (!fs19.existsSync(this.registryPath)) {
188920
189058
  return {
188921
189059
  version: 1,
188922
189060
  keys: {},
@@ -188924,7 +189062,7 @@ var ProxyRegistryManager = class {
188924
189062
  };
188925
189063
  }
188926
189064
  try {
188927
- const content = fs18.readFileSync(this.registryPath, "utf-8");
189065
+ const content = fs19.readFileSync(this.registryPath, "utf-8");
188928
189066
  return JSON.parse(content);
188929
189067
  } catch {
188930
189068
  return {
@@ -188937,8 +189075,8 @@ var ProxyRegistryManager = class {
188937
189075
  writeFileAtomic(filePath, data) {
188938
189076
  this.ensureProxyDir();
188939
189077
  const tmpPath = filePath + ".tmp";
188940
- fs18.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
188941
- fs18.renameSync(tmpPath, filePath);
189078
+ fs19.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
189079
+ fs19.renameSync(tmpPath, filePath);
188942
189080
  }
188943
189081
  };
188944
189082
 
@@ -189044,10 +189182,10 @@ function isInteractive() {
189044
189182
 
189045
189183
  // src/commands/dev.tsx
189046
189184
  var COLORS = ["cyan", "yellow", "green", "magenta", "blue"];
189047
- function DevUI({ instanceKey }) {
189185
+ function DevUI({ instanceKey, tunnelEnabled }) {
189048
189186
  const { exit } = useApp2();
189049
189187
  const [state, setState] = useState5(() => {
189050
- const caExists = caFilesExist();
189188
+ const caExists = tunnelEnabled || caFilesExist();
189051
189189
  return {
189052
189190
  status: caExists ? "loading" : "installing-ca",
189053
189191
  ...caExists ? {} : { caInstallPhase: "installing" },
@@ -189056,7 +189194,9 @@ function DevUI({ instanceKey }) {
189056
189194
  services: [],
189057
189195
  output: [],
189058
189196
  colorMap: /* @__PURE__ */ new Map(),
189059
- isProxyOwner: false
189197
+ isProxyOwner: false,
189198
+ tunnels: /* @__PURE__ */ new Map(),
189199
+ tunnelStatus: /* @__PURE__ */ new Map()
189060
189200
  };
189061
189201
  });
189062
189202
  useEffect3(() => {
@@ -189088,6 +189228,7 @@ function DevUI({ instanceKey }) {
189088
189228
  const drizzleGatewayRef = useRef(null);
189089
189229
  const registryWatcherCleanupRef = useRef(null);
189090
189230
  const electionIntervalRef = useRef(null);
189231
+ const tunnelsRef = useRef([]);
189091
189232
  const proxyRef = useRef(null);
189092
189233
  const adminServerRef = useRef(null);
189093
189234
  const servicesRef = useRef([]);
@@ -189121,7 +189262,9 @@ function DevUI({ instanceKey }) {
189121
189262
  // Stop Drizzle Gateway
189122
189263
  drizzleGatewayRef.current?.stop(),
189123
189264
  // Stop all resources
189124
- ...[...resourcesRef.current.values()].map((resource) => resource.stop())
189265
+ ...[...resourcesRef.current.values()].map((resource) => resource.stop()),
189266
+ // Stop all tunnels
189267
+ ...tunnelsRef.current.map((tunnel) => tunnel.stop())
189125
189268
  ]);
189126
189269
  if (proxyRegistryRef.current) {
189127
189270
  await proxyRegistryRef.current.unregisterServices(instanceKey);
@@ -189158,7 +189301,9 @@ function DevUI({ instanceKey }) {
189158
189301
  // Stop Drizzle Gateway
189159
189302
  drizzleGatewayRef.current?.stop(),
189160
189303
  // Stop all resources
189161
- ...[...resourcesRef.current.values()].map((resource) => resource.stop())
189304
+ ...[...resourcesRef.current.values()].map((resource) => resource.stop()),
189305
+ // Stop all tunnels
189306
+ ...tunnelsRef.current.map((tunnel) => tunnel.stop())
189162
189307
  ]);
189163
189308
  electricInstancesRef.current = [];
189164
189309
  drizzleGatewayRef.current = null;
@@ -189166,6 +189311,7 @@ function DevUI({ instanceKey }) {
189166
189311
  adminServerRef.current = null;
189167
189312
  servicesRef.current = [];
189168
189313
  resourcesRef.current = /* @__PURE__ */ new Map();
189314
+ tunnelsRef.current = [];
189169
189315
  if (proxyRegistryRef.current) {
189170
189316
  await proxyRegistryRef.current.unregisterServices(instanceKey);
189171
189317
  await proxyRegistryRef.current.releaseProxyOwnership();
@@ -189181,7 +189327,9 @@ function DevUI({ instanceKey }) {
189181
189327
  resourceStatus: /* @__PURE__ */ new Map(),
189182
189328
  services: [],
189183
189329
  proxy: void 0,
189184
- isProxyOwner: false
189330
+ isProxyOwner: false,
189331
+ tunnels: /* @__PURE__ */ new Map(),
189332
+ tunnelStatus: /* @__PURE__ */ new Map()
189185
189333
  }));
189186
189334
  setReloadTrigger((t) => t + 1);
189187
189335
  };
@@ -189222,10 +189370,10 @@ function DevUI({ instanceKey }) {
189222
189370
  }, [state.status]);
189223
189371
  useEffect3(() => {
189224
189372
  if (state.status !== "running") return;
189225
- const configPath = path16.join(process.cwd(), "specific.hcl");
189373
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189226
189374
  const watcher = watchConfigFile(configPath, 1e3, () => {
189227
189375
  try {
189228
- const hcl = fs19.readFileSync(configPath, "utf-8");
189376
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189229
189377
  parseConfig(hcl).then(() => {
189230
189378
  triggerReload();
189231
189379
  }).catch((err) => {
@@ -189350,8 +189498,8 @@ function DevUI({ instanceKey }) {
189350
189498
  }));
189351
189499
  return;
189352
189500
  }
189353
- const configPath = path16.join(process.cwd(), "specific.hcl");
189354
- if (!fs19.existsSync(configPath)) {
189501
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189502
+ if (!fs20.existsSync(configPath)) {
189355
189503
  writeLog("system", "Waiting for specific.hcl to appear");
189356
189504
  setState((s) => ({
189357
189505
  ...s,
@@ -189370,7 +189518,7 @@ function DevUI({ instanceKey }) {
189370
189518
  }
189371
189519
  let config2;
189372
189520
  try {
189373
- const hcl = fs19.readFileSync(configPath, "utf-8");
189521
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189374
189522
  config2 = await parseConfig(hcl);
189375
189523
  } catch (err) {
189376
189524
  setState((s) => ({
@@ -189476,7 +189624,7 @@ function DevUI({ instanceKey }) {
189476
189624
  const drizzleGateway = await startDrizzleGateway(
189477
189625
  postgresResources,
189478
189626
  drizzlePort,
189479
- path16.join(process.cwd(), ".specific", "keys", instanceKey)
189627
+ path17.join(process.cwd(), ".specific", "keys", instanceKey)
189480
189628
  );
189481
189629
  startedDrizzleGateway = drizzleGateway;
189482
189630
  drizzleGatewayRef.current = drizzleGateway;
@@ -189645,136 +189793,196 @@ Add them to the config block in specific.local`);
189645
189793
  }
189646
189794
  }
189647
189795
  }
189648
- const runningServicePorts = /* @__PURE__ */ new Map();
189649
- for (const s of services2) {
189650
- runningServicePorts.set(s.name, s.ports.get("default"));
189651
- }
189652
- const projectId = hasProjectId() ? readProjectId() : void 0;
189653
- const getState = () => ({
189654
- status: "running",
189655
- services: config2.services.filter((svc) => runningServicePorts.has(svc.name) || svc.serve).map((svc) => ({
189656
- name: svc.name,
189657
- port: runningServicePorts.get(svc.name),
189658
- exposed: !!svc.serve || svc.endpoints.some((e) => e.public),
189659
- env: svc.env
189660
- })),
189661
- resources: [...resources2.entries()].map(([name, r]) => ({
189662
- name,
189663
- type: r.type,
189664
- port: r.port,
189665
- host: r.host,
189666
- syncEnabled: r.type === "postgres" && syncDatabases.has(name)
189667
- })),
189668
- projectId
189669
- });
189670
- const adminServer = await startAdminServer(getState);
189671
- adminServerRef.current = adminServer;
189672
- writeLog("system", `Admin API server started on port ${adminServer.port}`);
189673
- await proxyRegistry.registerServices(
189674
- instanceKey,
189675
- adminServer.port,
189676
- serviceInfos,
189677
- startedDrizzleGateway?.port
189678
- );
189679
- writeLog("system", `Registered ${serviceInfos.length} services with proxy registry`);
189680
- const becameProxyOwner = await proxyRegistry.claimProxyOwnership(instanceKey);
189681
- if (becameProxyOwner) {
189682
- writeLog("system", "Claimed proxy ownership, starting HTTP proxy");
189683
- try {
189684
- const currentServices = await proxyRegistry.getAllServices();
189685
- const registeredKeys = [...new Set(currentServices.map((s) => s.key))];
189686
- const certificate = generateCertificate("local.spcf.app", registeredKeys);
189687
- const proxy2 = await startHttpProxy(
189688
- exposedServices,
189689
- certificate,
189690
- getState,
189691
- instanceKey
189692
- );
189693
- startedProxy = proxy2;
189694
- proxyRef.current = proxy2;
189695
- setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189696
- const knownKeys = new Set(registeredKeys);
189697
- registryWatcherCleanupRef.current = proxyRegistry.watchRegistry(async (updatedServices, updatedKeys) => {
189698
- writeLog("system", `Registry updated: ${updatedServices.length} services`);
189699
- proxy2.updateServices(updatedServices, updatedKeys);
189700
- const newKeyNames = Object.keys(updatedKeys).filter((k) => !knownKeys.has(k));
189701
- if (newKeyNames.length > 0) {
189702
- writeLog("system", `New keys detected: ${newKeyNames.join(", ")} - regenerating certificate`);
189703
- for (const key of newKeyNames) {
189704
- knownKeys.add(key);
189796
+ if (tunnelEnabled) {
189797
+ writeLog("system", "Tunnel mode enabled, starting tunnels for public services");
189798
+ if (exposedServices.length === 0) {
189799
+ writeLog("system", "No public services to tunnel");
189800
+ } else {
189801
+ const subdomainAllocator = new StableSubdomainAllocator(process.cwd(), instanceKey);
189802
+ const multipleServices = exposedServices.length > 1;
189803
+ const tunnelInstances = [];
189804
+ const tunnelStatusMap = /* @__PURE__ */ new Map();
189805
+ for (const svc of exposedServices) {
189806
+ tunnelStatusMap.set(svc.name, "connecting");
189807
+ }
189808
+ setState((s) => ({ ...s, tunnelStatus: new Map(tunnelStatusMap) }));
189809
+ for (const svc of exposedServices) {
189810
+ if (cancelled) return;
189811
+ const subdomain = subdomainAllocator.allocate(svc.name, multipleServices);
189812
+ writeLog("system", `Starting tunnel for ${svc.name} on port ${svc.port} (subdomain: ${subdomain})`);
189813
+ try {
189814
+ const tunnel = await startTunnel(
189815
+ svc.name,
189816
+ "default",
189817
+ svc.port,
189818
+ subdomain,
189819
+ {
189820
+ onError: (serviceName, _endpointName, error) => {
189821
+ writeLog("system:error", `Tunnel error for ${serviceName}: ${error.message}`);
189822
+ },
189823
+ onClose: (serviceName) => {
189824
+ writeLog("system", `Tunnel closed for ${serviceName}`);
189825
+ }
189826
+ }
189827
+ );
189828
+ tunnelInstances.push(tunnel);
189829
+ tunnelsRef.current = [...tunnelInstances];
189830
+ tunnelStatusMap.set(svc.name, "connected");
189831
+ setState((s) => ({
189832
+ ...s,
189833
+ tunnels: new Map([...s.tunnels, [svc.name, tunnel]]),
189834
+ tunnelStatus: new Map(tunnelStatusMap)
189835
+ }));
189836
+ writeLog("system", `Tunnel ready for ${svc.name}: ${tunnel.url}`);
189837
+ } catch (err) {
189838
+ const errorMsg = `Failed to start tunnel for ${svc.name}: ${err instanceof Error ? err.message : String(err)}`;
189839
+ writeLog("system:error", errorMsg);
189840
+ tunnelStatusMap.set(svc.name, "error");
189841
+ for (const t of tunnelInstances) {
189842
+ await t.stop();
189705
189843
  }
189706
- const allKeys = [...knownKeys];
189707
- const newCertificate = generateCertificate("local.spcf.app", allKeys);
189708
- proxy2.updateCertificate(newCertificate);
189844
+ setState((s) => ({
189845
+ ...s,
189846
+ status: "error",
189847
+ error: errorMsg,
189848
+ tunnelStatus: new Map(tunnelStatusMap)
189849
+ }));
189850
+ return;
189709
189851
  }
189710
- });
189711
- const currentKeys = await proxyRegistry.getAllKeyRegistrations();
189712
- proxy2.updateServices(currentServices, currentKeys);
189713
- writeLog("system", `Loaded ${currentServices.length} services from registry`);
189714
- } catch (err) {
189715
- const errorMsg = `Failed to start HTTP proxy: ${err instanceof Error ? err.message : String(err)}`;
189716
- writeLog("system:error", errorMsg);
189717
- setState((s) => ({
189718
- ...s,
189719
- status: "error",
189720
- error: errorMsg
189721
- }));
189722
- return;
189852
+ }
189723
189853
  }
189724
189854
  } else {
189725
- writeLog("system", "Another instance owns the proxy, starting election watcher");
189726
- setState((s) => ({ ...s, isProxyOwner: false }));
189727
- const isProcessRunning = (pid) => {
189855
+ const runningServicePorts = /* @__PURE__ */ new Map();
189856
+ for (const s of services2) {
189857
+ runningServicePorts.set(s.name, s.ports.get("default"));
189858
+ }
189859
+ const projectId = hasProjectId() ? readProjectId() : void 0;
189860
+ const getState = () => ({
189861
+ status: "running",
189862
+ services: config2.services.filter((svc) => runningServicePorts.has(svc.name) || svc.serve).map((svc) => ({
189863
+ name: svc.name,
189864
+ port: runningServicePorts.get(svc.name),
189865
+ exposed: !!svc.serve || svc.endpoints.some((e) => e.public),
189866
+ env: svc.env
189867
+ })),
189868
+ resources: [...resources2.entries()].map(([name, r]) => ({
189869
+ name,
189870
+ type: r.type,
189871
+ port: r.port,
189872
+ host: r.host,
189873
+ syncEnabled: r.type === "postgres" && syncDatabases.has(name)
189874
+ })),
189875
+ projectId
189876
+ });
189877
+ const adminServer = await startAdminServer(getState);
189878
+ adminServerRef.current = adminServer;
189879
+ writeLog("system", `Admin API server started on port ${adminServer.port}`);
189880
+ await proxyRegistry.registerServices(
189881
+ instanceKey,
189882
+ adminServer.port,
189883
+ serviceInfos,
189884
+ startedDrizzleGateway?.port
189885
+ );
189886
+ writeLog("system", `Registered ${serviceInfos.length} services with proxy registry`);
189887
+ const becameProxyOwner = await proxyRegistry.claimProxyOwnership(instanceKey);
189888
+ if (becameProxyOwner) {
189889
+ writeLog("system", "Claimed proxy ownership, starting HTTP proxy");
189728
189890
  try {
189729
- process.kill(pid, 0);
189730
- return true;
189731
- } catch {
189732
- return false;
189733
- }
189734
- };
189735
- electionIntervalRef.current = setInterval(async () => {
189736
- if (cancelled || shuttingDown.current) {
189737
- if (electionIntervalRef.current) {
189738
- clearInterval(electionIntervalRef.current);
189739
- electionIntervalRef.current = null;
189740
- }
189891
+ const currentServices = await proxyRegistry.getAllServices();
189892
+ const registeredKeys = [...new Set(currentServices.map((s) => s.key))];
189893
+ const certificate = generateCertificate("local.spcf.app", registeredKeys);
189894
+ const proxy2 = await startHttpProxy(
189895
+ exposedServices,
189896
+ certificate,
189897
+ getState,
189898
+ instanceKey
189899
+ );
189900
+ startedProxy = proxy2;
189901
+ proxyRef.current = proxy2;
189902
+ setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189903
+ const knownKeys = new Set(registeredKeys);
189904
+ registryWatcherCleanupRef.current = proxyRegistry.watchRegistry(async (updatedServices, updatedKeys) => {
189905
+ writeLog("system", `Registry updated: ${updatedServices.length} services`);
189906
+ proxy2.updateServices(updatedServices, updatedKeys);
189907
+ const newKeyNames = Object.keys(updatedKeys).filter((k) => !knownKeys.has(k));
189908
+ if (newKeyNames.length > 0) {
189909
+ writeLog("system", `New keys detected: ${newKeyNames.join(", ")} - regenerating certificate`);
189910
+ for (const key of newKeyNames) {
189911
+ knownKeys.add(key);
189912
+ }
189913
+ const allKeys = [...knownKeys];
189914
+ const newCertificate = generateCertificate("local.spcf.app", allKeys);
189915
+ proxy2.updateCertificate(newCertificate);
189916
+ }
189917
+ });
189918
+ const currentKeys = await proxyRegistry.getAllKeyRegistrations();
189919
+ proxy2.updateServices(currentServices, currentKeys);
189920
+ writeLog("system", `Loaded ${currentServices.length} services from registry`);
189921
+ } catch (err) {
189922
+ const errorMsg = `Failed to start HTTP proxy: ${err instanceof Error ? err.message : String(err)}`;
189923
+ writeLog("system:error", errorMsg);
189924
+ setState((s) => ({
189925
+ ...s,
189926
+ status: "error",
189927
+ error: errorMsg
189928
+ }));
189741
189929
  return;
189742
189930
  }
189743
- const owner = await proxyRegistry.getProxyOwner();
189744
- if (!owner || !isProcessRunning(owner.pid)) {
189745
- writeLog("system", "Proxy owner died, attempting election");
189746
- const won = await proxyRegistry.attemptElection(instanceKey);
189747
- if (won) {
189748
- writeLog("system", "Won election, starting HTTP proxy");
189931
+ } else {
189932
+ writeLog("system", "Another instance owns the proxy, starting election watcher");
189933
+ setState((s) => ({ ...s, isProxyOwner: false }));
189934
+ const isProcessRunning = (pid) => {
189935
+ try {
189936
+ process.kill(pid, 0);
189937
+ return true;
189938
+ } catch {
189939
+ return false;
189940
+ }
189941
+ };
189942
+ electionIntervalRef.current = setInterval(async () => {
189943
+ if (cancelled || shuttingDown.current) {
189749
189944
  if (electionIntervalRef.current) {
189750
189945
  clearInterval(electionIntervalRef.current);
189751
189946
  electionIntervalRef.current = null;
189752
189947
  }
189753
- try {
189754
- const electionServices = await proxyRegistry.getAllServices();
189755
- const electionKeyRegistrations = await proxyRegistry.getAllKeyRegistrations();
189756
- const electionKeyNames = Object.keys(electionKeyRegistrations);
189757
- const certificate = generateCertificate("local.spcf.app", electionKeyNames);
189758
- const proxy2 = await startHttpProxy(
189759
- exposedServices,
189760
- certificate,
189761
- getState,
189762
- instanceKey
189763
- );
189764
- startedProxy = proxy2;
189765
- proxyRef.current = proxy2;
189766
- setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189767
- registryWatcherCleanupRef.current = proxyRegistry.watchRegistry((updatedServices, updatedKeys) => {
189768
- writeLog("system", `Registry updated: ${updatedServices.length} services`);
189769
- proxy2.updateServices(updatedServices, updatedKeys);
189770
- });
189771
- proxy2.updateServices(electionServices, electionKeyRegistrations);
189772
- } catch (err) {
189773
- writeLog("system:error", `Failed to start proxy after election: ${err}`);
189948
+ return;
189949
+ }
189950
+ const owner = await proxyRegistry.getProxyOwner();
189951
+ if (!owner || !isProcessRunning(owner.pid)) {
189952
+ writeLog("system", "Proxy owner died, attempting election");
189953
+ const won = await proxyRegistry.attemptElection(instanceKey);
189954
+ if (won) {
189955
+ writeLog("system", "Won election, starting HTTP proxy");
189956
+ if (electionIntervalRef.current) {
189957
+ clearInterval(electionIntervalRef.current);
189958
+ electionIntervalRef.current = null;
189959
+ }
189960
+ try {
189961
+ const electionServices = await proxyRegistry.getAllServices();
189962
+ const electionKeyRegistrations = await proxyRegistry.getAllKeyRegistrations();
189963
+ const electionKeyNames = Object.keys(electionKeyRegistrations);
189964
+ const certificate = generateCertificate("local.spcf.app", electionKeyNames);
189965
+ const proxy2 = await startHttpProxy(
189966
+ exposedServices,
189967
+ certificate,
189968
+ getState,
189969
+ instanceKey
189970
+ );
189971
+ startedProxy = proxy2;
189972
+ proxyRef.current = proxy2;
189973
+ setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189974
+ registryWatcherCleanupRef.current = proxyRegistry.watchRegistry((updatedServices, updatedKeys) => {
189975
+ writeLog("system", `Registry updated: ${updatedServices.length} services`);
189976
+ proxy2.updateServices(updatedServices, updatedKeys);
189977
+ });
189978
+ proxy2.updateServices(electionServices, electionKeyRegistrations);
189979
+ } catch (err) {
189980
+ writeLog("system:error", `Failed to start proxy after election: ${err}`);
189981
+ }
189774
189982
  }
189775
189983
  }
189776
- }
189777
- }, 1e3);
189984
+ }, 1e3);
189985
+ }
189778
189986
  }
189779
189987
  if (cancelled) return;
189780
189988
  writeLog("system", "Dev server running");
@@ -189920,39 +190128,58 @@ Add them to the config block in specific.local`);
189920
190128
  const staticItems = [
189921
190129
  {
189922
190130
  key: "title",
189923
- content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), instanceKey !== "default" && /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " [", instanceKey, "]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)"))
190131
+ content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true, color: "cyan" }, "Specific dev server"), tunnelEnabled && /* @__PURE__ */ React6.createElement(Text6, { color: "magenta" }, " [tunnel]"), instanceKey !== "default" && /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, " [", instanceKey, "]"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (Ctrl+C to stop)"))
189924
190132
  },
189925
190133
  { key: "space1", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") },
189926
- // Show admin UI URL
189927
- {
189928
- key: "admin",
189929
- content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Admin:"), /* @__PURE__ */ React6.createElement(Text6, null, " "), /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "https://", instanceKey === "default" ? "" : `${instanceKey}.`, "local.spcf.app"))
189930
- },
189931
- { key: "admin-space", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") },
189932
- ...services.length > 0 ? [
189933
- { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
189934
- ...services.flatMap((svc) => {
189935
- const serviceConfig = config.services.find((s) => s.name === svc.name);
189936
- const endpoints = serviceConfig?.endpoints || [];
189937
- if (endpoints.length === 0 && svc.ports.size > 0) {
189938
- const defaultPort = svc.ports.get("default");
189939
- return [{
189940
- key: `svc-${svc.name}`,
189941
- content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, " \u25CF "), /* @__PURE__ */ React6.createElement(Text6, null, svc.name), defaultPort && /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", defaultPort, ")"))
189942
- }];
189943
- }
189944
- return endpoints.map((endpoint) => {
189945
- const port = svc.ports.get(endpoint.name);
189946
- const displayName = endpoint.name === "default" ? svc.name : `${svc.name}:${endpoint.name}`;
189947
- const proxyName = endpoint.name === "default" ? svc.name : `${svc.name}-${endpoint.name}`;
190134
+ // Show admin UI URL (only in non-tunnel mode)
190135
+ ...!tunnelEnabled ? [
190136
+ {
190137
+ key: "admin",
190138
+ content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Admin:"), /* @__PURE__ */ React6.createElement(Text6, null, " "), /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "https://", instanceKey === "default" ? "" : `${instanceKey}.`, "local.spcf.app"))
190139
+ },
190140
+ { key: "admin-space", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190141
+ ] : [],
190142
+ // Services section - different rendering for tunnel mode vs local mode
190143
+ ...tunnelEnabled ? (
190144
+ // Tunnel mode: show tunnel URLs for public services
190145
+ state.tunnels.size > 0 || state.tunnelStatus.size > 0 ? [
190146
+ { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
190147
+ ...[...state.tunnelStatus.entries()].map(([serviceName, status]) => {
190148
+ const tunnel = state.tunnels.get(serviceName);
189948
190149
  return {
189949
- key: `svc-${svc.name}-${endpoint.name}`,
189950
- content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, " \u25CF "), /* @__PURE__ */ React6.createElement(Text6, null, displayName), port ? endpoint.public ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, null, " \u2192 "), /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "https://", proxyName, instanceKey === "default" ? "" : `.${instanceKey}`, ".local.spcf.app"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", port, ")")) : /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", port, ")") : null)
190150
+ key: `svc-${serviceName}`,
190151
+ content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: status === "connected" ? "green" : status === "error" ? "red" : "yellow" }, " \u25CF "), /* @__PURE__ */ React6.createElement(Text6, null, serviceName), status === "connected" && tunnel ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, null, " \u2192 "), /* @__PURE__ */ React6.createElement(Text6, { bold: true }, tunnel.url)) : status === "connecting" ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " connecting...") : /* @__PURE__ */ React6.createElement(Text6, { color: "red" }, " error"))
189951
190152
  };
189952
- });
189953
- }),
189954
- { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
189955
- ] : [],
190153
+ }),
190154
+ { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190155
+ ] : []
190156
+ ) : (
190157
+ // Local mode: show local URLs
190158
+ services.length > 0 ? [
190159
+ { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
190160
+ ...services.flatMap((svc) => {
190161
+ const serviceConfig = config.services.find((s) => s.name === svc.name);
190162
+ const endpoints = serviceConfig?.endpoints || [];
190163
+ if (endpoints.length === 0 && svc.ports.size > 0) {
190164
+ const defaultPort = svc.ports.get("default");
190165
+ return [{
190166
+ key: `svc-${svc.name}`,
190167
+ content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, " \u25CF "), /* @__PURE__ */ React6.createElement(Text6, null, svc.name), defaultPort && /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", defaultPort, ")"))
190168
+ }];
190169
+ }
190170
+ return endpoints.map((endpoint) => {
190171
+ const port = svc.ports.get(endpoint.name);
190172
+ const displayName = endpoint.name === "default" ? svc.name : `${svc.name}:${endpoint.name}`;
190173
+ const proxyName = endpoint.name === "default" ? svc.name : `${svc.name}-${endpoint.name}`;
190174
+ return {
190175
+ key: `svc-${svc.name}-${endpoint.name}`,
190176
+ content: /* @__PURE__ */ React6.createElement(Text6, null, /* @__PURE__ */ React6.createElement(Text6, { color: "green" }, " \u25CF "), /* @__PURE__ */ React6.createElement(Text6, null, displayName), port ? endpoint.public ? /* @__PURE__ */ React6.createElement(React6.Fragment, null, /* @__PURE__ */ React6.createElement(Text6, null, " \u2192 "), /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "https://", proxyName, instanceKey === "default" ? "" : `.${instanceKey}`, ".local.spcf.app"), /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", port, ")")) : /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, " (localhost:", port, ")") : null)
190177
+ };
190178
+ });
190179
+ }),
190180
+ { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190181
+ ] : []
190182
+ ),
189956
190183
  ...config.postgres.length > 0 ? [
189957
190184
  { key: "pg-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Postgres:") },
189958
190185
  ...config.postgres.map((pg) => {
@@ -190000,13 +190227,13 @@ Add them to the config block in specific.local`);
190000
190227
  ];
190001
190228
  return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Static, { items: staticItems }, (item) => /* @__PURE__ */ React6.createElement(Box6, { key: item.key }, item.content)));
190002
190229
  }
190003
- function devCommand(instanceKey) {
190004
- render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey }));
190230
+ function devCommand(instanceKey, tunnelEnabled = false) {
190231
+ render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey, tunnelEnabled }));
190005
190232
  }
190006
190233
 
190007
190234
  // src/lib/dev/git-worktree.ts
190008
190235
  import { execSync as execSync2 } from "child_process";
190009
- import * as path17 from "path";
190236
+ import * as path18 from "path";
190010
190237
  function isInWorktree() {
190011
190238
  try {
190012
190239
  const commonDir = execSync2("git rev-parse --git-common-dir", {
@@ -190017,8 +190244,8 @@ function isInWorktree() {
190017
190244
  encoding: "utf-8",
190018
190245
  stdio: ["pipe", "pipe", "pipe"]
190019
190246
  }).trim();
190020
- const resolvedCommonDir = path17.resolve(commonDir);
190021
- const resolvedGitDir = path17.resolve(gitDir);
190247
+ const resolvedCommonDir = path18.resolve(commonDir);
190248
+ const resolvedGitDir = path18.resolve(gitDir);
190022
190249
  return resolvedCommonDir !== resolvedGitDir;
190023
190250
  } catch {
190024
190251
  return false;
@@ -190033,7 +190260,7 @@ function getWorktreeName() {
190033
190260
  encoding: "utf-8",
190034
190261
  stdio: ["pipe", "pipe", "pipe"]
190035
190262
  }).trim();
190036
- return path17.basename(gitDir);
190263
+ return path18.basename(gitDir);
190037
190264
  } catch {
190038
190265
  return null;
190039
190266
  }
@@ -190048,35 +190275,35 @@ init_open();
190048
190275
  import React7, { useState as useState6, useEffect as useEffect4, useCallback } from "react";
190049
190276
  import { render as render5, Text as Text7, Box as Box7, useApp as useApp3, useInput as useInput5 } from "ink";
190050
190277
  import Spinner5 from "ink-spinner";
190051
- import * as fs21 from "fs";
190052
- import * as path19 from "path";
190278
+ import * as fs22 from "fs";
190279
+ import * as path20 from "path";
190053
190280
 
190054
190281
  // src/lib/deploy/build-tester.ts
190055
190282
  import { spawn as spawn5 } from "child_process";
190056
- import { existsSync as existsSync17 } from "fs";
190057
- import { join as join18 } from "path";
190283
+ import { existsSync as existsSync18 } from "fs";
190284
+ import { join as join19 } from "path";
190058
190285
  function getDependencyInstallCommand(build, projectDir) {
190059
190286
  switch (build.base) {
190060
190287
  case "node":
190061
- if (existsSync17(join18(projectDir, "pnpm-lock.yaml"))) {
190288
+ if (existsSync18(join19(projectDir, "pnpm-lock.yaml"))) {
190062
190289
  return "pnpm install --frozen-lockfile";
190063
- } else if (existsSync17(join18(projectDir, "yarn.lock"))) {
190290
+ } else if (existsSync18(join19(projectDir, "yarn.lock"))) {
190064
190291
  return "yarn install --frozen-lockfile";
190065
- } else if (existsSync17(join18(projectDir, "package-lock.json"))) {
190292
+ } else if (existsSync18(join19(projectDir, "package-lock.json"))) {
190066
190293
  return "npm ci";
190067
190294
  } else {
190068
190295
  return "npm install";
190069
190296
  }
190070
190297
  case "python":
190071
- if (existsSync17(join18(projectDir, "poetry.lock"))) {
190298
+ if (existsSync18(join19(projectDir, "poetry.lock"))) {
190072
190299
  return "poetry install --no-interaction";
190073
- } else if (existsSync17(join18(projectDir, "Pipfile.lock"))) {
190300
+ } else if (existsSync18(join19(projectDir, "Pipfile.lock"))) {
190074
190301
  return "pipenv install --deploy";
190075
- } else if (existsSync17(join18(projectDir, "Pipfile"))) {
190302
+ } else if (existsSync18(join19(projectDir, "Pipfile"))) {
190076
190303
  return "pipenv install";
190077
- } else if (existsSync17(join18(projectDir, "pyproject.toml"))) {
190304
+ } else if (existsSync18(join19(projectDir, "pyproject.toml"))) {
190078
190305
  return "pip install .";
190079
- } else if (existsSync17(join18(projectDir, "requirements.txt"))) {
190306
+ } else if (existsSync18(join19(projectDir, "requirements.txt"))) {
190080
190307
  return "pip install -r requirements.txt";
190081
190308
  }
190082
190309
  return null;
@@ -190222,8 +190449,8 @@ async function testAllBuilds(builds, projectDir) {
190222
190449
 
190223
190450
  // src/lib/tarball/create.ts
190224
190451
  import { execSync as execSync3 } from "child_process";
190225
- import * as fs20 from "fs";
190226
- import * as path18 from "path";
190452
+ import * as fs21 from "fs";
190453
+ import * as path19 from "path";
190227
190454
  import { createTarPacker, createEntryItemGenerator } from "tar-vern";
190228
190455
  function isInsideGitRepository(dir) {
190229
190456
  try {
@@ -190280,10 +190507,10 @@ var EXCLUDED_DIRS = [
190280
190507
  ];
190281
190508
  async function collectPaths(baseDir, currentDir, exclude) {
190282
190509
  const results = [];
190283
- const entries = await fs20.promises.readdir(currentDir, { withFileTypes: true });
190510
+ const entries = await fs21.promises.readdir(currentDir, { withFileTypes: true });
190284
190511
  for (const entry of entries) {
190285
- const fullPath = path18.join(currentDir, entry.name);
190286
- const relativePath = path18.relative(baseDir, fullPath);
190512
+ const fullPath = path19.join(currentDir, entry.name);
190513
+ const relativePath = path19.relative(baseDir, fullPath);
190287
190514
  if (entry.isDirectory()) {
190288
190515
  if (!exclude.includes(entry.name)) {
190289
190516
  results.push(relativePath);
@@ -190298,8 +190525,8 @@ async function collectPaths(baseDir, currentDir, exclude) {
190298
190525
  }
190299
190526
  async function createTarArchive(projectDir) {
190300
190527
  writeLog("tarball", "Creating tarball using tar-vern (non-git project)");
190301
- const configPath = path18.join(projectDir, "specific.hcl");
190302
- if (!fs20.existsSync(configPath)) {
190528
+ const configPath = path19.join(projectDir, "specific.hcl");
190529
+ if (!fs21.existsSync(configPath)) {
190303
190530
  throw new Error("specific.hcl not found in project directory");
190304
190531
  }
190305
190532
  const relativePaths = await collectPaths(projectDir, projectDir, EXCLUDED_DIRS);
@@ -190316,8 +190543,8 @@ async function createTarArchive(projectDir) {
190316
190543
  }
190317
190544
  function findWidestContext(projectDir, contexts) {
190318
190545
  if (contexts.length === 0) return ".";
190319
- const absolute = contexts.map((c) => path18.resolve(projectDir, c));
190320
- const segments = absolute.map((p) => p.split(path18.sep).filter(Boolean));
190546
+ const absolute = contexts.map((c) => path19.resolve(projectDir, c));
190547
+ const segments = absolute.map((p) => p.split(path19.sep).filter(Boolean));
190321
190548
  const firstSegments = segments[0];
190322
190549
  if (!firstSegments) return ".";
190323
190550
  const minLen = Math.min(...segments.map((s) => s.length));
@@ -190331,12 +190558,12 @@ function findWidestContext(projectDir, contexts) {
190331
190558
  }
190332
190559
  }
190333
190560
  const ancestorSegments = firstSegments.slice(0, commonLength);
190334
- const ancestor = path18.sep + ancestorSegments.join(path18.sep);
190335
- return path18.relative(projectDir, ancestor) || ".";
190561
+ const ancestor = path19.sep + ancestorSegments.join(path19.sep);
190562
+ return path19.relative(projectDir, ancestor) || ".";
190336
190563
  }
190337
190564
  async function createProjectTarball(projectDir, context = ".") {
190338
- const contextDir = path18.resolve(projectDir, context);
190339
- const appPath = path18.relative(contextDir, projectDir) || ".";
190565
+ const contextDir = path19.resolve(projectDir, context);
190566
+ const appPath = path19.relative(contextDir, projectDir) || ".";
190340
190567
  writeLog("tarball", `Context: ${contextDir}, appPath: ${appPath}`);
190341
190568
  let tarball;
190342
190569
  if (isInsideGitRepository(contextDir)) {
@@ -191223,14 +191450,14 @@ ${errorMsg}`
191223
191450
  ), phase === "error" && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "red" }, "Error: ", error)), phase === "success" && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { color: "green" }, "Deployment successful!"), deployment?.publicUrls && Object.keys(deployment.publicUrls).length > 0 && /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1, flexDirection: "column" }, /* @__PURE__ */ React7.createElement(Text7, { bold: true }, "Public URLs:"), Object.entries(deployment.publicUrls).map(([name, url]) => /* @__PURE__ */ React7.createElement(Text7, { key: name }, " ", name, ": ", /* @__PURE__ */ React7.createElement(Text7, { color: "cyan" }, url))))));
191224
191451
  }
191225
191452
  async function deployCommand(environment, options2) {
191226
- const configPath = path19.join(process.cwd(), "specific.hcl");
191227
- if (!fs21.existsSync(configPath)) {
191453
+ const configPath = path20.join(process.cwd(), "specific.hcl");
191454
+ if (!fs22.existsSync(configPath)) {
191228
191455
  console.error("Error: No specific.hcl found in current directory");
191229
191456
  process.exit(1);
191230
191457
  }
191231
191458
  let config;
191232
191459
  try {
191233
- const hcl = fs21.readFileSync(configPath, "utf-8");
191460
+ const hcl = fs22.readFileSync(configPath, "utf-8");
191234
191461
  config = await parseConfig(hcl);
191235
191462
  } catch (err) {
191236
191463
  console.error(
@@ -191254,8 +191481,8 @@ async function deployCommand(environment, options2) {
191254
191481
 
191255
191482
  // src/commands/exec.tsx
191256
191483
  import { spawn as spawn6 } from "child_process";
191257
- import * as fs22 from "fs";
191258
- import * as path20 from "path";
191484
+ import * as fs23 from "fs";
191485
+ import * as path21 from "path";
191259
191486
  async function execCommand(serviceName, command, instanceKey = "default") {
191260
191487
  if (command.length === 0) {
191261
191488
  console.error(
@@ -191283,14 +191510,14 @@ async function execCommand(serviceName, command, instanceKey = "default") {
191283
191510
  }
191284
191511
  }
191285
191512
  };
191286
- const configPath = path20.join(process.cwd(), "specific.hcl");
191287
- if (!fs22.existsSync(configPath)) {
191513
+ const configPath = path21.join(process.cwd(), "specific.hcl");
191514
+ if (!fs23.existsSync(configPath)) {
191288
191515
  console.error("Error: No specific.hcl found in current directory");
191289
191516
  process.exit(1);
191290
191517
  }
191291
191518
  let config;
191292
191519
  try {
191293
- const hcl = fs22.readFileSync(configPath, "utf-8");
191520
+ const hcl = fs23.readFileSync(configPath, "utf-8");
191294
191521
  config = await parseConfig(hcl);
191295
191522
  } catch (err) {
191296
191523
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -191484,21 +191711,21 @@ async function psqlCommand(databaseName, instanceKey = "default") {
191484
191711
  import React8, { useState as useState7, useEffect as useEffect5 } from "react";
191485
191712
  import { render as render6, Text as Text8, Box as Box8 } from "ink";
191486
191713
  import Spinner6 from "ink-spinner";
191487
- import * as fs23 from "fs";
191488
- import * as path21 from "path";
191714
+ import * as fs24 from "fs";
191715
+ import * as path22 from "path";
191489
191716
  function CleanUI({ instanceKey }) {
191490
191717
  const [state, setState] = useState7({ status: "checking" });
191491
191718
  useEffect5(() => {
191492
191719
  async function clean() {
191493
191720
  const projectRoot = process.cwd();
191494
- const specificDir = path21.join(projectRoot, ".specific");
191495
- if (!fs23.existsSync(specificDir)) {
191721
+ const specificDir = path22.join(projectRoot, ".specific");
191722
+ if (!fs24.existsSync(specificDir)) {
191496
191723
  setState({ status: "nothing" });
191497
191724
  return;
191498
191725
  }
191499
191726
  if (instanceKey) {
191500
- const keyDir = path21.join(specificDir, "keys", instanceKey);
191501
- if (!fs23.existsSync(keyDir)) {
191727
+ const keyDir = path22.join(specificDir, "keys", instanceKey);
191728
+ if (!fs24.existsSync(keyDir)) {
191502
191729
  setState({ status: "nothing" });
191503
191730
  return;
191504
191731
  }
@@ -191514,7 +191741,7 @@ function CleanUI({ instanceKey }) {
191514
191741
  await stateManager.cleanStaleState();
191515
191742
  setState({ status: "cleaning" });
191516
191743
  try {
191517
- fs23.rmSync(keyDir, { recursive: true, force: true });
191744
+ fs24.rmSync(keyDir, { recursive: true, force: true });
191518
191745
  setState({ status: "success" });
191519
191746
  } catch (err) {
191520
191747
  setState({
@@ -191523,10 +191750,10 @@ function CleanUI({ instanceKey }) {
191523
191750
  });
191524
191751
  }
191525
191752
  } else {
191526
- const keysDir = path21.join(specificDir, "keys");
191527
- if (fs23.existsSync(keysDir)) {
191528
- const keys = fs23.readdirSync(keysDir).filter(
191529
- (f) => fs23.statSync(path21.join(keysDir, f)).isDirectory()
191753
+ const keysDir = path22.join(specificDir, "keys");
191754
+ if (fs24.existsSync(keysDir)) {
191755
+ const keys = fs24.readdirSync(keysDir).filter(
191756
+ (f) => fs24.statSync(path22.join(keysDir, f)).isDirectory()
191530
191757
  );
191531
191758
  for (const key of keys) {
191532
191759
  const stateManager2 = new InstanceStateManager(projectRoot, key);
@@ -191551,7 +191778,7 @@ function CleanUI({ instanceKey }) {
191551
191778
  }
191552
191779
  setState({ status: "cleaning" });
191553
191780
  try {
191554
- fs23.rmSync(specificDir, { recursive: true, force: true });
191781
+ fs24.rmSync(specificDir, { recursive: true, force: true });
191555
191782
  setState({ status: "success" });
191556
191783
  } catch (err) {
191557
191784
  setState({
@@ -191638,13 +191865,13 @@ function logoutCommand() {
191638
191865
  var program = new Command();
191639
191866
  var env = "production";
191640
191867
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
191641
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.46").enablePositionalOptions();
191868
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.48").enablePositionalOptions();
191642
191869
  program.command("init").description("Initialize project for use with a coding agent").action(initCommand);
191643
191870
  program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
191644
191871
  program.command("check").description("Validate specific.hcl configuration").action(checkCommand);
191645
- program.command("dev").description("Start local development environment").option("-k, --key <key>", "Namespace for isolated dev environment (auto-detected from git worktree if not specified)").action((options2) => {
191872
+ program.command("dev").description("Start local development environment").option("-k, --key <key>", "Namespace for isolated dev environment (auto-detected from git worktree if not specified)").option("--tunnel", "Expose public services via localtunnel URLs").action((options2) => {
191646
191873
  const key = options2.key ?? getDefaultKey();
191647
- devCommand(key);
191874
+ devCommand(key, options2.tunnel ?? false);
191648
191875
  });
191649
191876
  program.command("deploy [environment]").description("Deploy to Specific infrastructure").option("--skip-build-test", "Skip local build testing before deploy").action((environment, options2) => {
191650
191877
  deployCommand(environment, options2);