@specific.dev/cli 0.1.47 → 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 (30) 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 +1 -1
  4. package/dist/admin/__next._full.txt +1 -1
  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/_not-found/__next._full.txt +1 -1
  9. package/dist/admin/_not-found/__next._head.txt +1 -1
  10. package/dist/admin/_not-found/__next._index.txt +1 -1
  11. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  13. package/dist/admin/_not-found/__next._tree.txt +1 -1
  14. package/dist/admin/_not-found/index.html +1 -1
  15. package/dist/admin/_not-found/index.txt +1 -1
  16. package/dist/admin/databases/__next._full.txt +1 -1
  17. package/dist/admin/databases/__next._head.txt +1 -1
  18. package/dist/admin/databases/__next._index.txt +1 -1
  19. package/dist/admin/databases/__next._tree.txt +1 -1
  20. package/dist/admin/databases/__next.databases.__PAGE__.txt +1 -1
  21. package/dist/admin/databases/__next.databases.txt +1 -1
  22. package/dist/admin/databases/index.html +1 -1
  23. package/dist/admin/databases/index.txt +1 -1
  24. package/dist/admin/index.html +1 -1
  25. package/dist/admin/index.txt +1 -1
  26. package/dist/cli.js +586 -400
  27. package/package.json +5 -2
  28. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → dyH4SZNKyN31L1iV-yPZA}/_buildManifest.js +0 -0
  29. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → dyH4SZNKyN31L1iV-yPZA}/_clientMiddlewareManifest.json +0 -0
  30. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → 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.47",
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,9 +188620,106 @@ 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";
188627
188724
  import * as net4 from "net";
188628
188725
  var ProxyRegistryManager = class {
@@ -188633,14 +188730,14 @@ var ProxyRegistryManager = class {
188633
188730
  isOwner = false;
188634
188731
  registryWatcher = null;
188635
188732
  constructor() {
188636
- this.proxyDir = path15.join(os8.homedir(), ".specific", "proxy");
188637
- this.ownerPath = path15.join(this.proxyDir, "owner.json");
188638
- this.registryPath = path15.join(this.proxyDir, "registry.json");
188639
- 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");
188640
188737
  }
188641
188738
  ensureProxyDir() {
188642
- if (!fs18.existsSync(this.proxyDir)) {
188643
- fs18.mkdirSync(this.proxyDir, { recursive: true });
188739
+ if (!fs19.existsSync(this.proxyDir)) {
188740
+ fs19.mkdirSync(this.proxyDir, { recursive: true });
188644
188741
  }
188645
188742
  }
188646
188743
  isProcessRunning(pid) {
@@ -188697,15 +188794,15 @@ var ProxyRegistryManager = class {
188697
188794
  const startTime = Date.now();
188698
188795
  while (Date.now() - startTime < timeoutMs) {
188699
188796
  try {
188700
- const fd = fs18.openSync(
188797
+ const fd = fs19.openSync(
188701
188798
  this.lockPath,
188702
- 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
188703
188800
  );
188704
- fs18.writeSync(fd, String(process.pid));
188705
- fs18.closeSync(fd);
188801
+ fs19.writeSync(fd, String(process.pid));
188802
+ fs19.closeSync(fd);
188706
188803
  return () => {
188707
188804
  try {
188708
- fs18.unlinkSync(this.lockPath);
188805
+ fs19.unlinkSync(this.lockPath);
188709
188806
  } catch {
188710
188807
  }
188711
188808
  };
@@ -188714,16 +188811,16 @@ var ProxyRegistryManager = class {
188714
188811
  if (err.code === "EEXIST") {
188715
188812
  try {
188716
188813
  const lockPid = parseInt(
188717
- fs18.readFileSync(this.lockPath, "utf-8").trim(),
188814
+ fs19.readFileSync(this.lockPath, "utf-8").trim(),
188718
188815
  10
188719
188816
  );
188720
188817
  if (!this.isProcessRunning(lockPid)) {
188721
- fs18.unlinkSync(this.lockPath);
188818
+ fs19.unlinkSync(this.lockPath);
188722
188819
  continue;
188723
188820
  }
188724
188821
  } catch {
188725
188822
  try {
188726
- fs18.unlinkSync(this.lockPath);
188823
+ fs19.unlinkSync(this.lockPath);
188727
188824
  } catch {
188728
188825
  }
188729
188826
  continue;
@@ -188743,8 +188840,8 @@ var ProxyRegistryManager = class {
188743
188840
  async claimProxyOwnership(key) {
188744
188841
  const releaseLock = await this.acquireLock();
188745
188842
  try {
188746
- if (fs18.existsSync(this.ownerPath)) {
188747
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188843
+ if (fs19.existsSync(this.ownerPath)) {
188844
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188748
188845
  const ownerFile2 = JSON.parse(content);
188749
188846
  if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188750
188847
  return false;
@@ -188774,11 +188871,11 @@ var ProxyRegistryManager = class {
188774
188871
  }
188775
188872
  const releaseLock = await this.acquireLock();
188776
188873
  try {
188777
- if (fs18.existsSync(this.ownerPath)) {
188778
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188874
+ if (fs19.existsSync(this.ownerPath)) {
188875
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188779
188876
  const ownerFile = JSON.parse(content);
188780
188877
  if (ownerFile.owner.pid === process.pid) {
188781
- fs18.unlinkSync(this.ownerPath);
188878
+ fs19.unlinkSync(this.ownerPath);
188782
188879
  }
188783
188880
  }
188784
188881
  this.isOwner = false;
@@ -188790,12 +188887,12 @@ var ProxyRegistryManager = class {
188790
188887
  * Get the current proxy owner.
188791
188888
  */
188792
188889
  async getProxyOwner() {
188793
- if (!fs18.existsSync(this.ownerPath)) {
188890
+ if (!fs19.existsSync(this.ownerPath)) {
188794
188891
  return null;
188795
188892
  }
188796
188893
  const releaseLock = await this.acquireLock();
188797
188894
  try {
188798
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188895
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188799
188896
  const ownerFile = JSON.parse(content);
188800
188897
  if (!await this.isProxyOwnerHealthy(ownerFile.owner.pid)) {
188801
188898
  return null;
@@ -188889,7 +188986,7 @@ var ProxyRegistryManager = class {
188889
188986
  */
188890
188987
  watchRegistry(onChange) {
188891
188988
  this.ensureProxyDir();
188892
- if (!fs18.existsSync(this.registryPath)) {
188989
+ if (!fs19.existsSync(this.registryPath)) {
188893
188990
  const emptyRegistry = {
188894
188991
  version: 1,
188895
188992
  keys: {},
@@ -188933,13 +189030,13 @@ var ProxyRegistryManager = class {
188933
189030
  async attemptElection(key) {
188934
189031
  const releaseLock = await this.acquireLock();
188935
189032
  try {
188936
- if (fs18.existsSync(this.ownerPath)) {
188937
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
189033
+ if (fs19.existsSync(this.ownerPath)) {
189034
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188938
189035
  const ownerFile2 = JSON.parse(content);
188939
189036
  if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188940
189037
  return false;
188941
189038
  }
188942
- fs18.unlinkSync(this.ownerPath);
189039
+ fs19.unlinkSync(this.ownerPath);
188943
189040
  }
188944
189041
  const ownerFile = {
188945
189042
  version: 1,
@@ -188957,7 +189054,7 @@ var ProxyRegistryManager = class {
188957
189054
  }
188958
189055
  }
188959
189056
  readRegistry() {
188960
- if (!fs18.existsSync(this.registryPath)) {
189057
+ if (!fs19.existsSync(this.registryPath)) {
188961
189058
  return {
188962
189059
  version: 1,
188963
189060
  keys: {},
@@ -188965,7 +189062,7 @@ var ProxyRegistryManager = class {
188965
189062
  };
188966
189063
  }
188967
189064
  try {
188968
- const content = fs18.readFileSync(this.registryPath, "utf-8");
189065
+ const content = fs19.readFileSync(this.registryPath, "utf-8");
188969
189066
  return JSON.parse(content);
188970
189067
  } catch {
188971
189068
  return {
@@ -188978,8 +189075,8 @@ var ProxyRegistryManager = class {
188978
189075
  writeFileAtomic(filePath, data) {
188979
189076
  this.ensureProxyDir();
188980
189077
  const tmpPath = filePath + ".tmp";
188981
- fs18.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
188982
- fs18.renameSync(tmpPath, filePath);
189078
+ fs19.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
189079
+ fs19.renameSync(tmpPath, filePath);
188983
189080
  }
188984
189081
  };
188985
189082
 
@@ -189085,10 +189182,10 @@ function isInteractive() {
189085
189182
 
189086
189183
  // src/commands/dev.tsx
189087
189184
  var COLORS = ["cyan", "yellow", "green", "magenta", "blue"];
189088
- function DevUI({ instanceKey }) {
189185
+ function DevUI({ instanceKey, tunnelEnabled }) {
189089
189186
  const { exit } = useApp2();
189090
189187
  const [state, setState] = useState5(() => {
189091
- const caExists = caFilesExist();
189188
+ const caExists = tunnelEnabled || caFilesExist();
189092
189189
  return {
189093
189190
  status: caExists ? "loading" : "installing-ca",
189094
189191
  ...caExists ? {} : { caInstallPhase: "installing" },
@@ -189097,7 +189194,9 @@ function DevUI({ instanceKey }) {
189097
189194
  services: [],
189098
189195
  output: [],
189099
189196
  colorMap: /* @__PURE__ */ new Map(),
189100
- isProxyOwner: false
189197
+ isProxyOwner: false,
189198
+ tunnels: /* @__PURE__ */ new Map(),
189199
+ tunnelStatus: /* @__PURE__ */ new Map()
189101
189200
  };
189102
189201
  });
189103
189202
  useEffect3(() => {
@@ -189129,6 +189228,7 @@ function DevUI({ instanceKey }) {
189129
189228
  const drizzleGatewayRef = useRef(null);
189130
189229
  const registryWatcherCleanupRef = useRef(null);
189131
189230
  const electionIntervalRef = useRef(null);
189231
+ const tunnelsRef = useRef([]);
189132
189232
  const proxyRef = useRef(null);
189133
189233
  const adminServerRef = useRef(null);
189134
189234
  const servicesRef = useRef([]);
@@ -189162,7 +189262,9 @@ function DevUI({ instanceKey }) {
189162
189262
  // Stop Drizzle Gateway
189163
189263
  drizzleGatewayRef.current?.stop(),
189164
189264
  // Stop all resources
189165
- ...[...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())
189166
189268
  ]);
189167
189269
  if (proxyRegistryRef.current) {
189168
189270
  await proxyRegistryRef.current.unregisterServices(instanceKey);
@@ -189199,7 +189301,9 @@ function DevUI({ instanceKey }) {
189199
189301
  // Stop Drizzle Gateway
189200
189302
  drizzleGatewayRef.current?.stop(),
189201
189303
  // Stop all resources
189202
- ...[...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())
189203
189307
  ]);
189204
189308
  electricInstancesRef.current = [];
189205
189309
  drizzleGatewayRef.current = null;
@@ -189207,6 +189311,7 @@ function DevUI({ instanceKey }) {
189207
189311
  adminServerRef.current = null;
189208
189312
  servicesRef.current = [];
189209
189313
  resourcesRef.current = /* @__PURE__ */ new Map();
189314
+ tunnelsRef.current = [];
189210
189315
  if (proxyRegistryRef.current) {
189211
189316
  await proxyRegistryRef.current.unregisterServices(instanceKey);
189212
189317
  await proxyRegistryRef.current.releaseProxyOwnership();
@@ -189222,7 +189327,9 @@ function DevUI({ instanceKey }) {
189222
189327
  resourceStatus: /* @__PURE__ */ new Map(),
189223
189328
  services: [],
189224
189329
  proxy: void 0,
189225
- isProxyOwner: false
189330
+ isProxyOwner: false,
189331
+ tunnels: /* @__PURE__ */ new Map(),
189332
+ tunnelStatus: /* @__PURE__ */ new Map()
189226
189333
  }));
189227
189334
  setReloadTrigger((t) => t + 1);
189228
189335
  };
@@ -189263,10 +189370,10 @@ function DevUI({ instanceKey }) {
189263
189370
  }, [state.status]);
189264
189371
  useEffect3(() => {
189265
189372
  if (state.status !== "running") return;
189266
- const configPath = path16.join(process.cwd(), "specific.hcl");
189373
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189267
189374
  const watcher = watchConfigFile(configPath, 1e3, () => {
189268
189375
  try {
189269
- const hcl = fs19.readFileSync(configPath, "utf-8");
189376
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189270
189377
  parseConfig(hcl).then(() => {
189271
189378
  triggerReload();
189272
189379
  }).catch((err) => {
@@ -189391,8 +189498,8 @@ function DevUI({ instanceKey }) {
189391
189498
  }));
189392
189499
  return;
189393
189500
  }
189394
- const configPath = path16.join(process.cwd(), "specific.hcl");
189395
- if (!fs19.existsSync(configPath)) {
189501
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189502
+ if (!fs20.existsSync(configPath)) {
189396
189503
  writeLog("system", "Waiting for specific.hcl to appear");
189397
189504
  setState((s) => ({
189398
189505
  ...s,
@@ -189411,7 +189518,7 @@ function DevUI({ instanceKey }) {
189411
189518
  }
189412
189519
  let config2;
189413
189520
  try {
189414
- const hcl = fs19.readFileSync(configPath, "utf-8");
189521
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189415
189522
  config2 = await parseConfig(hcl);
189416
189523
  } catch (err) {
189417
189524
  setState((s) => ({
@@ -189517,7 +189624,7 @@ function DevUI({ instanceKey }) {
189517
189624
  const drizzleGateway = await startDrizzleGateway(
189518
189625
  postgresResources,
189519
189626
  drizzlePort,
189520
- path16.join(process.cwd(), ".specific", "keys", instanceKey)
189627
+ path17.join(process.cwd(), ".specific", "keys", instanceKey)
189521
189628
  );
189522
189629
  startedDrizzleGateway = drizzleGateway;
189523
189630
  drizzleGatewayRef.current = drizzleGateway;
@@ -189686,136 +189793,196 @@ Add them to the config block in specific.local`);
189686
189793
  }
189687
189794
  }
189688
189795
  }
189689
- const runningServicePorts = /* @__PURE__ */ new Map();
189690
- for (const s of services2) {
189691
- runningServicePorts.set(s.name, s.ports.get("default"));
189692
- }
189693
- const projectId = hasProjectId() ? readProjectId() : void 0;
189694
- const getState = () => ({
189695
- status: "running",
189696
- services: config2.services.filter((svc) => runningServicePorts.has(svc.name) || svc.serve).map((svc) => ({
189697
- name: svc.name,
189698
- port: runningServicePorts.get(svc.name),
189699
- exposed: !!svc.serve || svc.endpoints.some((e) => e.public),
189700
- env: svc.env
189701
- })),
189702
- resources: [...resources2.entries()].map(([name, r]) => ({
189703
- name,
189704
- type: r.type,
189705
- port: r.port,
189706
- host: r.host,
189707
- syncEnabled: r.type === "postgres" && syncDatabases.has(name)
189708
- })),
189709
- projectId
189710
- });
189711
- const adminServer = await startAdminServer(getState);
189712
- adminServerRef.current = adminServer;
189713
- writeLog("system", `Admin API server started on port ${adminServer.port}`);
189714
- await proxyRegistry.registerServices(
189715
- instanceKey,
189716
- adminServer.port,
189717
- serviceInfos,
189718
- startedDrizzleGateway?.port
189719
- );
189720
- writeLog("system", `Registered ${serviceInfos.length} services with proxy registry`);
189721
- const becameProxyOwner = await proxyRegistry.claimProxyOwnership(instanceKey);
189722
- if (becameProxyOwner) {
189723
- writeLog("system", "Claimed proxy ownership, starting HTTP proxy");
189724
- try {
189725
- const currentServices = await proxyRegistry.getAllServices();
189726
- const registeredKeys = [...new Set(currentServices.map((s) => s.key))];
189727
- const certificate = generateCertificate("local.spcf.app", registeredKeys);
189728
- const proxy2 = await startHttpProxy(
189729
- exposedServices,
189730
- certificate,
189731
- getState,
189732
- instanceKey
189733
- );
189734
- startedProxy = proxy2;
189735
- proxyRef.current = proxy2;
189736
- setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189737
- const knownKeys = new Set(registeredKeys);
189738
- registryWatcherCleanupRef.current = proxyRegistry.watchRegistry(async (updatedServices, updatedKeys) => {
189739
- writeLog("system", `Registry updated: ${updatedServices.length} services`);
189740
- proxy2.updateServices(updatedServices, updatedKeys);
189741
- const newKeyNames = Object.keys(updatedKeys).filter((k) => !knownKeys.has(k));
189742
- if (newKeyNames.length > 0) {
189743
- writeLog("system", `New keys detected: ${newKeyNames.join(", ")} - regenerating certificate`);
189744
- for (const key of newKeyNames) {
189745
- 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();
189746
189843
  }
189747
- const allKeys = [...knownKeys];
189748
- const newCertificate = generateCertificate("local.spcf.app", allKeys);
189749
- proxy2.updateCertificate(newCertificate);
189844
+ setState((s) => ({
189845
+ ...s,
189846
+ status: "error",
189847
+ error: errorMsg,
189848
+ tunnelStatus: new Map(tunnelStatusMap)
189849
+ }));
189850
+ return;
189750
189851
  }
189751
- });
189752
- const currentKeys = await proxyRegistry.getAllKeyRegistrations();
189753
- proxy2.updateServices(currentServices, currentKeys);
189754
- writeLog("system", `Loaded ${currentServices.length} services from registry`);
189755
- } catch (err) {
189756
- const errorMsg = `Failed to start HTTP proxy: ${err instanceof Error ? err.message : String(err)}`;
189757
- writeLog("system:error", errorMsg);
189758
- setState((s) => ({
189759
- ...s,
189760
- status: "error",
189761
- error: errorMsg
189762
- }));
189763
- return;
189852
+ }
189764
189853
  }
189765
189854
  } else {
189766
- writeLog("system", "Another instance owns the proxy, starting election watcher");
189767
- setState((s) => ({ ...s, isProxyOwner: false }));
189768
- 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");
189769
189890
  try {
189770
- process.kill(pid, 0);
189771
- return true;
189772
- } catch {
189773
- return false;
189774
- }
189775
- };
189776
- electionIntervalRef.current = setInterval(async () => {
189777
- if (cancelled || shuttingDown.current) {
189778
- if (electionIntervalRef.current) {
189779
- clearInterval(electionIntervalRef.current);
189780
- electionIntervalRef.current = null;
189781
- }
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
+ }));
189782
189929
  return;
189783
189930
  }
189784
- const owner = await proxyRegistry.getProxyOwner();
189785
- if (!owner || !isProcessRunning(owner.pid)) {
189786
- writeLog("system", "Proxy owner died, attempting election");
189787
- const won = await proxyRegistry.attemptElection(instanceKey);
189788
- if (won) {
189789
- 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) {
189790
189944
  if (electionIntervalRef.current) {
189791
189945
  clearInterval(electionIntervalRef.current);
189792
189946
  electionIntervalRef.current = null;
189793
189947
  }
189794
- try {
189795
- const electionServices = await proxyRegistry.getAllServices();
189796
- const electionKeyRegistrations = await proxyRegistry.getAllKeyRegistrations();
189797
- const electionKeyNames = Object.keys(electionKeyRegistrations);
189798
- const certificate = generateCertificate("local.spcf.app", electionKeyNames);
189799
- const proxy2 = await startHttpProxy(
189800
- exposedServices,
189801
- certificate,
189802
- getState,
189803
- instanceKey
189804
- );
189805
- startedProxy = proxy2;
189806
- proxyRef.current = proxy2;
189807
- setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
189808
- registryWatcherCleanupRef.current = proxyRegistry.watchRegistry((updatedServices, updatedKeys) => {
189809
- writeLog("system", `Registry updated: ${updatedServices.length} services`);
189810
- proxy2.updateServices(updatedServices, updatedKeys);
189811
- });
189812
- proxy2.updateServices(electionServices, electionKeyRegistrations);
189813
- } catch (err) {
189814
- 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
+ }
189815
189982
  }
189816
189983
  }
189817
- }
189818
- }, 1e3);
189984
+ }, 1e3);
189985
+ }
189819
189986
  }
189820
189987
  if (cancelled) return;
189821
189988
  writeLog("system", "Dev server running");
@@ -189961,39 +190128,58 @@ Add them to the config block in specific.local`);
189961
190128
  const staticItems = [
189962
190129
  {
189963
190130
  key: "title",
189964
- 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)"))
189965
190132
  },
189966
190133
  { key: "space1", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") },
189967
- // Show admin UI URL
189968
- {
189969
- key: "admin",
189970
- 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"))
189971
- },
189972
- { key: "admin-space", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") },
189973
- ...services.length > 0 ? [
189974
- { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
189975
- ...services.flatMap((svc) => {
189976
- const serviceConfig = config.services.find((s) => s.name === svc.name);
189977
- const endpoints = serviceConfig?.endpoints || [];
189978
- if (endpoints.length === 0 && svc.ports.size > 0) {
189979
- const defaultPort = svc.ports.get("default");
189980
- return [{
189981
- key: `svc-${svc.name}`,
189982
- 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, ")"))
189983
- }];
189984
- }
189985
- return endpoints.map((endpoint) => {
189986
- const port = svc.ports.get(endpoint.name);
189987
- const displayName = endpoint.name === "default" ? svc.name : `${svc.name}:${endpoint.name}`;
189988
- 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);
189989
190149
  return {
189990
- key: `svc-${svc.name}-${endpoint.name}`,
189991
- 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"))
189992
190152
  };
189993
- });
189994
- }),
189995
- { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
189996
- ] : [],
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
+ ),
189997
190183
  ...config.postgres.length > 0 ? [
189998
190184
  { key: "pg-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Postgres:") },
189999
190185
  ...config.postgres.map((pg) => {
@@ -190041,13 +190227,13 @@ Add them to the config block in specific.local`);
190041
190227
  ];
190042
190228
  return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Static, { items: staticItems }, (item) => /* @__PURE__ */ React6.createElement(Box6, { key: item.key }, item.content)));
190043
190229
  }
190044
- function devCommand(instanceKey) {
190045
- render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey }));
190230
+ function devCommand(instanceKey, tunnelEnabled = false) {
190231
+ render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey, tunnelEnabled }));
190046
190232
  }
190047
190233
 
190048
190234
  // src/lib/dev/git-worktree.ts
190049
190235
  import { execSync as execSync2 } from "child_process";
190050
- import * as path17 from "path";
190236
+ import * as path18 from "path";
190051
190237
  function isInWorktree() {
190052
190238
  try {
190053
190239
  const commonDir = execSync2("git rev-parse --git-common-dir", {
@@ -190058,8 +190244,8 @@ function isInWorktree() {
190058
190244
  encoding: "utf-8",
190059
190245
  stdio: ["pipe", "pipe", "pipe"]
190060
190246
  }).trim();
190061
- const resolvedCommonDir = path17.resolve(commonDir);
190062
- const resolvedGitDir = path17.resolve(gitDir);
190247
+ const resolvedCommonDir = path18.resolve(commonDir);
190248
+ const resolvedGitDir = path18.resolve(gitDir);
190063
190249
  return resolvedCommonDir !== resolvedGitDir;
190064
190250
  } catch {
190065
190251
  return false;
@@ -190074,7 +190260,7 @@ function getWorktreeName() {
190074
190260
  encoding: "utf-8",
190075
190261
  stdio: ["pipe", "pipe", "pipe"]
190076
190262
  }).trim();
190077
- return path17.basename(gitDir);
190263
+ return path18.basename(gitDir);
190078
190264
  } catch {
190079
190265
  return null;
190080
190266
  }
@@ -190089,35 +190275,35 @@ init_open();
190089
190275
  import React7, { useState as useState6, useEffect as useEffect4, useCallback } from "react";
190090
190276
  import { render as render5, Text as Text7, Box as Box7, useApp as useApp3, useInput as useInput5 } from "ink";
190091
190277
  import Spinner5 from "ink-spinner";
190092
- import * as fs21 from "fs";
190093
- import * as path19 from "path";
190278
+ import * as fs22 from "fs";
190279
+ import * as path20 from "path";
190094
190280
 
190095
190281
  // src/lib/deploy/build-tester.ts
190096
190282
  import { spawn as spawn5 } from "child_process";
190097
- import { existsSync as existsSync17 } from "fs";
190098
- import { join as join18 } from "path";
190283
+ import { existsSync as existsSync18 } from "fs";
190284
+ import { join as join19 } from "path";
190099
190285
  function getDependencyInstallCommand(build, projectDir) {
190100
190286
  switch (build.base) {
190101
190287
  case "node":
190102
- if (existsSync17(join18(projectDir, "pnpm-lock.yaml"))) {
190288
+ if (existsSync18(join19(projectDir, "pnpm-lock.yaml"))) {
190103
190289
  return "pnpm install --frozen-lockfile";
190104
- } else if (existsSync17(join18(projectDir, "yarn.lock"))) {
190290
+ } else if (existsSync18(join19(projectDir, "yarn.lock"))) {
190105
190291
  return "yarn install --frozen-lockfile";
190106
- } else if (existsSync17(join18(projectDir, "package-lock.json"))) {
190292
+ } else if (existsSync18(join19(projectDir, "package-lock.json"))) {
190107
190293
  return "npm ci";
190108
190294
  } else {
190109
190295
  return "npm install";
190110
190296
  }
190111
190297
  case "python":
190112
- if (existsSync17(join18(projectDir, "poetry.lock"))) {
190298
+ if (existsSync18(join19(projectDir, "poetry.lock"))) {
190113
190299
  return "poetry install --no-interaction";
190114
- } else if (existsSync17(join18(projectDir, "Pipfile.lock"))) {
190300
+ } else if (existsSync18(join19(projectDir, "Pipfile.lock"))) {
190115
190301
  return "pipenv install --deploy";
190116
- } else if (existsSync17(join18(projectDir, "Pipfile"))) {
190302
+ } else if (existsSync18(join19(projectDir, "Pipfile"))) {
190117
190303
  return "pipenv install";
190118
- } else if (existsSync17(join18(projectDir, "pyproject.toml"))) {
190304
+ } else if (existsSync18(join19(projectDir, "pyproject.toml"))) {
190119
190305
  return "pip install .";
190120
- } else if (existsSync17(join18(projectDir, "requirements.txt"))) {
190306
+ } else if (existsSync18(join19(projectDir, "requirements.txt"))) {
190121
190307
  return "pip install -r requirements.txt";
190122
190308
  }
190123
190309
  return null;
@@ -190263,8 +190449,8 @@ async function testAllBuilds(builds, projectDir) {
190263
190449
 
190264
190450
  // src/lib/tarball/create.ts
190265
190451
  import { execSync as execSync3 } from "child_process";
190266
- import * as fs20 from "fs";
190267
- import * as path18 from "path";
190452
+ import * as fs21 from "fs";
190453
+ import * as path19 from "path";
190268
190454
  import { createTarPacker, createEntryItemGenerator } from "tar-vern";
190269
190455
  function isInsideGitRepository(dir) {
190270
190456
  try {
@@ -190321,10 +190507,10 @@ var EXCLUDED_DIRS = [
190321
190507
  ];
190322
190508
  async function collectPaths(baseDir, currentDir, exclude) {
190323
190509
  const results = [];
190324
- const entries = await fs20.promises.readdir(currentDir, { withFileTypes: true });
190510
+ const entries = await fs21.promises.readdir(currentDir, { withFileTypes: true });
190325
190511
  for (const entry of entries) {
190326
- const fullPath = path18.join(currentDir, entry.name);
190327
- const relativePath = path18.relative(baseDir, fullPath);
190512
+ const fullPath = path19.join(currentDir, entry.name);
190513
+ const relativePath = path19.relative(baseDir, fullPath);
190328
190514
  if (entry.isDirectory()) {
190329
190515
  if (!exclude.includes(entry.name)) {
190330
190516
  results.push(relativePath);
@@ -190339,8 +190525,8 @@ async function collectPaths(baseDir, currentDir, exclude) {
190339
190525
  }
190340
190526
  async function createTarArchive(projectDir) {
190341
190527
  writeLog("tarball", "Creating tarball using tar-vern (non-git project)");
190342
- const configPath = path18.join(projectDir, "specific.hcl");
190343
- if (!fs20.existsSync(configPath)) {
190528
+ const configPath = path19.join(projectDir, "specific.hcl");
190529
+ if (!fs21.existsSync(configPath)) {
190344
190530
  throw new Error("specific.hcl not found in project directory");
190345
190531
  }
190346
190532
  const relativePaths = await collectPaths(projectDir, projectDir, EXCLUDED_DIRS);
@@ -190357,8 +190543,8 @@ async function createTarArchive(projectDir) {
190357
190543
  }
190358
190544
  function findWidestContext(projectDir, contexts) {
190359
190545
  if (contexts.length === 0) return ".";
190360
- const absolute = contexts.map((c) => path18.resolve(projectDir, c));
190361
- 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));
190362
190548
  const firstSegments = segments[0];
190363
190549
  if (!firstSegments) return ".";
190364
190550
  const minLen = Math.min(...segments.map((s) => s.length));
@@ -190372,12 +190558,12 @@ function findWidestContext(projectDir, contexts) {
190372
190558
  }
190373
190559
  }
190374
190560
  const ancestorSegments = firstSegments.slice(0, commonLength);
190375
- const ancestor = path18.sep + ancestorSegments.join(path18.sep);
190376
- return path18.relative(projectDir, ancestor) || ".";
190561
+ const ancestor = path19.sep + ancestorSegments.join(path19.sep);
190562
+ return path19.relative(projectDir, ancestor) || ".";
190377
190563
  }
190378
190564
  async function createProjectTarball(projectDir, context = ".") {
190379
- const contextDir = path18.resolve(projectDir, context);
190380
- const appPath = path18.relative(contextDir, projectDir) || ".";
190565
+ const contextDir = path19.resolve(projectDir, context);
190566
+ const appPath = path19.relative(contextDir, projectDir) || ".";
190381
190567
  writeLog("tarball", `Context: ${contextDir}, appPath: ${appPath}`);
190382
190568
  let tarball;
190383
190569
  if (isInsideGitRepository(contextDir)) {
@@ -191264,14 +191450,14 @@ ${errorMsg}`
191264
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))))));
191265
191451
  }
191266
191452
  async function deployCommand(environment, options2) {
191267
- const configPath = path19.join(process.cwd(), "specific.hcl");
191268
- if (!fs21.existsSync(configPath)) {
191453
+ const configPath = path20.join(process.cwd(), "specific.hcl");
191454
+ if (!fs22.existsSync(configPath)) {
191269
191455
  console.error("Error: No specific.hcl found in current directory");
191270
191456
  process.exit(1);
191271
191457
  }
191272
191458
  let config;
191273
191459
  try {
191274
- const hcl = fs21.readFileSync(configPath, "utf-8");
191460
+ const hcl = fs22.readFileSync(configPath, "utf-8");
191275
191461
  config = await parseConfig(hcl);
191276
191462
  } catch (err) {
191277
191463
  console.error(
@@ -191295,8 +191481,8 @@ async function deployCommand(environment, options2) {
191295
191481
 
191296
191482
  // src/commands/exec.tsx
191297
191483
  import { spawn as spawn6 } from "child_process";
191298
- import * as fs22 from "fs";
191299
- import * as path20 from "path";
191484
+ import * as fs23 from "fs";
191485
+ import * as path21 from "path";
191300
191486
  async function execCommand(serviceName, command, instanceKey = "default") {
191301
191487
  if (command.length === 0) {
191302
191488
  console.error(
@@ -191324,14 +191510,14 @@ async function execCommand(serviceName, command, instanceKey = "default") {
191324
191510
  }
191325
191511
  }
191326
191512
  };
191327
- const configPath = path20.join(process.cwd(), "specific.hcl");
191328
- if (!fs22.existsSync(configPath)) {
191513
+ const configPath = path21.join(process.cwd(), "specific.hcl");
191514
+ if (!fs23.existsSync(configPath)) {
191329
191515
  console.error("Error: No specific.hcl found in current directory");
191330
191516
  process.exit(1);
191331
191517
  }
191332
191518
  let config;
191333
191519
  try {
191334
- const hcl = fs22.readFileSync(configPath, "utf-8");
191520
+ const hcl = fs23.readFileSync(configPath, "utf-8");
191335
191521
  config = await parseConfig(hcl);
191336
191522
  } catch (err) {
191337
191523
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -191525,21 +191711,21 @@ async function psqlCommand(databaseName, instanceKey = "default") {
191525
191711
  import React8, { useState as useState7, useEffect as useEffect5 } from "react";
191526
191712
  import { render as render6, Text as Text8, Box as Box8 } from "ink";
191527
191713
  import Spinner6 from "ink-spinner";
191528
- import * as fs23 from "fs";
191529
- import * as path21 from "path";
191714
+ import * as fs24 from "fs";
191715
+ import * as path22 from "path";
191530
191716
  function CleanUI({ instanceKey }) {
191531
191717
  const [state, setState] = useState7({ status: "checking" });
191532
191718
  useEffect5(() => {
191533
191719
  async function clean() {
191534
191720
  const projectRoot = process.cwd();
191535
- const specificDir = path21.join(projectRoot, ".specific");
191536
- if (!fs23.existsSync(specificDir)) {
191721
+ const specificDir = path22.join(projectRoot, ".specific");
191722
+ if (!fs24.existsSync(specificDir)) {
191537
191723
  setState({ status: "nothing" });
191538
191724
  return;
191539
191725
  }
191540
191726
  if (instanceKey) {
191541
- const keyDir = path21.join(specificDir, "keys", instanceKey);
191542
- if (!fs23.existsSync(keyDir)) {
191727
+ const keyDir = path22.join(specificDir, "keys", instanceKey);
191728
+ if (!fs24.existsSync(keyDir)) {
191543
191729
  setState({ status: "nothing" });
191544
191730
  return;
191545
191731
  }
@@ -191555,7 +191741,7 @@ function CleanUI({ instanceKey }) {
191555
191741
  await stateManager.cleanStaleState();
191556
191742
  setState({ status: "cleaning" });
191557
191743
  try {
191558
- fs23.rmSync(keyDir, { recursive: true, force: true });
191744
+ fs24.rmSync(keyDir, { recursive: true, force: true });
191559
191745
  setState({ status: "success" });
191560
191746
  } catch (err) {
191561
191747
  setState({
@@ -191564,10 +191750,10 @@ function CleanUI({ instanceKey }) {
191564
191750
  });
191565
191751
  }
191566
191752
  } else {
191567
- const keysDir = path21.join(specificDir, "keys");
191568
- if (fs23.existsSync(keysDir)) {
191569
- const keys = fs23.readdirSync(keysDir).filter(
191570
- (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()
191571
191757
  );
191572
191758
  for (const key of keys) {
191573
191759
  const stateManager2 = new InstanceStateManager(projectRoot, key);
@@ -191592,7 +191778,7 @@ function CleanUI({ instanceKey }) {
191592
191778
  }
191593
191779
  setState({ status: "cleaning" });
191594
191780
  try {
191595
- fs23.rmSync(specificDir, { recursive: true, force: true });
191781
+ fs24.rmSync(specificDir, { recursive: true, force: true });
191596
191782
  setState({ status: "success" });
191597
191783
  } catch (err) {
191598
191784
  setState({
@@ -191679,13 +191865,13 @@ function logoutCommand() {
191679
191865
  var program = new Command();
191680
191866
  var env = "production";
191681
191867
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
191682
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.47").enablePositionalOptions();
191868
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.48").enablePositionalOptions();
191683
191869
  program.command("init").description("Initialize project for use with a coding agent").action(initCommand);
191684
191870
  program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
191685
191871
  program.command("check").description("Validate specific.hcl configuration").action(checkCommand);
191686
- 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) => {
191687
191873
  const key = options2.key ?? getDefaultKey();
191688
- devCommand(key);
191874
+ devCommand(key, options2.tunnel ?? false);
191689
191875
  });
191690
191876
  program.command("deploy [environment]").description("Deploy to Specific infrastructure").option("--skip-build-test", "Skip local build testing before deploy").action((environment, options2) => {
191691
191877
  deployCommand(environment, options2);