@specific.dev/cli 0.1.47 → 0.1.49

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 (34) 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 +889 -477
  27. package/dist/docs/builds.md +22 -0
  28. package/dist/docs/postgres.md +15 -0
  29. package/dist/docs/services.md +62 -8
  30. package/dist/postinstall.js +141 -0
  31. package/package.json +7 -3
  32. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → FMaKxl-Dpw5U-PgHqIMag}/_buildManifest.js +0 -0
  33. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → FMaKxl-Dpw5U-PgHqIMag}/_clientMiddlewareManifest.json +0 -0
  34. /package/dist/admin/_next/static/{pcYHo7d7--ealoH3_ELSO → FMaKxl-Dpw5U-PgHqIMag}/_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 (path24) => {
242
+ if (/^[a-z]+:\/\//i.test(path24)) {
243
+ return path24;
244
244
  }
245
245
  try {
246
- const { stdout } = await execFile2("wslpath", ["-aw", path22], { encoding: "utf8" });
246
+ const { stdout } = await execFile2("wslpath", ["-aw", path24], { encoding: "utf8" });
247
247
  return stdout.trim();
248
248
  } catch {
249
- return path22;
249
+ return path24;
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 fs26 = $global.require("fs");
758
+ "object" == typeof fs26 && null !== fs26 && 0 !== Object.keys(fs26).length && ($global.fs = fs26);
759
759
  } catch (e) {
760
760
  }
761
761
  if (!$global.fs) {
@@ -183055,13 +183055,13 @@ function removeCA() {
183055
183055
  }
183056
183056
  }
183057
183057
  function installCAToTrustStore(certPath) {
183058
- const platform5 = os.platform();
183059
- if (platform5 === "darwin") {
183058
+ const platform4 = os.platform();
183059
+ if (platform4 === "darwin") {
183060
183060
  execSync(
183061
183061
  `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`,
183062
183062
  { stdio: "inherit" }
183063
183063
  );
183064
- } else if (platform5 === "linux") {
183064
+ } else if (platform4 === "linux") {
183065
183065
  if (fs.existsSync("/usr/local/share/ca-certificates")) {
183066
183066
  execSync(
183067
183067
  `sudo cp "${certPath}" /usr/local/share/ca-certificates/specific-local-ca.crt`,
@@ -183078,7 +183078,7 @@ function installCAToTrustStore(certPath) {
183078
183078
  throw new Error("Could not detect Linux certificate trust mechanism");
183079
183079
  }
183080
183080
  } else {
183081
- throw new Error(`Unsupported platform: ${platform5}`);
183081
+ throw new Error(`Unsupported platform: ${platform4}`);
183082
183082
  }
183083
183083
  }
183084
183084
  function generateCertificate(domain, keys = []) {
@@ -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.49",
183877
183877
  platform: process.platform,
183878
183878
  node_version: process.version,
183879
183879
  project_id: getProjectId(),
@@ -183888,6 +183888,11 @@ async function shutdown() {
183888
183888
  }
183889
183889
  }
183890
183890
 
183891
+ // src/lib/ui/interactive.ts
183892
+ function isInteractive() {
183893
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
183894
+ }
183895
+
183891
183896
  // src/commands/init.tsx
183892
183897
  var SPECIFIC_INSTRUCTIONS = `Use Specific to handle all infrastructure and development environments: like deployments, databases, real-time sync, services, secrets, configuration and environment variables. Run \`specific docs\` to learn how to use Specific. ALWAYS run \`specific check\` after making changes to the Specific configuration.`;
183893
183898
  var CURSOR_MDC_CONTENT = `---
@@ -184156,8 +184161,77 @@ function InitUI() {
184156
184161
  " "
184157
184162
  ))));
184158
184163
  }
184159
- function initCommand() {
184160
- render2(/* @__PURE__ */ React2.createElement(InitUI, null));
184164
+ var VALID_AGENT_IDS = options.map((o) => o.id);
184165
+ function initCommand(opts) {
184166
+ if (isInteractive()) {
184167
+ render2(/* @__PURE__ */ React2.createElement(InitUI, null));
184168
+ return;
184169
+ }
184170
+ if (!opts.agent || opts.agent.length === 0) {
184171
+ console.error(
184172
+ `Error: --agent is required in non-interactive environments.
184173
+ Usage: specific init --agent cursor claude codex
184174
+ Valid agents: ${VALID_AGENT_IDS.join(", ")}`
184175
+ );
184176
+ process.exit(1);
184177
+ }
184178
+ const invalid = opts.agent.filter((a) => !VALID_AGENT_IDS.includes(a));
184179
+ if (invalid.length > 0) {
184180
+ console.error(
184181
+ `Error: Unknown agent(s): ${invalid.join(", ")}
184182
+ Valid agents: ${VALID_AGENT_IDS.join(", ")}`
184183
+ );
184184
+ process.exit(1);
184185
+ }
184186
+ const checked = {};
184187
+ for (const agent of opts.agent) {
184188
+ checked[agent] = true;
184189
+ }
184190
+ const result = configureAgents(checked);
184191
+ trackEvent("project_initialized", {
184192
+ agents: opts.agent
184193
+ });
184194
+ const selectedAgents = opts.agent.length;
184195
+ const agentChanges = result.agents.filesCreated.length > 0 || result.agents.filesModified.length > 0;
184196
+ console.log("\u2713 Coding agents configured");
184197
+ if (result.agents.filesCreated.length > 0) {
184198
+ console.log(` Created: ${result.agents.filesCreated.join(", ")}`);
184199
+ }
184200
+ if (result.agents.filesModified.length > 0) {
184201
+ console.log(` Modified: ${result.agents.filesModified.join(", ")}`);
184202
+ }
184203
+ if (!agentChanges && selectedAgents > 0) {
184204
+ console.log(" No changes needed (files already configured)");
184205
+ }
184206
+ if (result.git) {
184207
+ const gitChanges = result.git.filesCreated.length > 0 || result.git.filesModified.length > 0;
184208
+ console.log("\u2713 Git configured");
184209
+ if (result.git.filesCreated.length > 0) {
184210
+ console.log(` Created: ${result.git.filesCreated.join(", ")}`);
184211
+ }
184212
+ if (result.git.filesModified.length > 0) {
184213
+ console.log(` Modified: ${result.git.filesModified.join(", ")}`);
184214
+ }
184215
+ if (!gitChanges) {
184216
+ console.log(" No changes needed (.gitignore already configured)");
184217
+ }
184218
+ }
184219
+ if (!caFilesExist()) {
184220
+ console.log(
184221
+ "\u26A0 TLS certificates not installed (run `specific init` in a terminal to set up)"
184222
+ );
184223
+ }
184224
+ if (result.showManualInstructions) {
184225
+ console.log("\n\u2713 Manual configuration selected");
184226
+ console.log("\n Add this to your agent's system prompt:");
184227
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
184228
+ console.log(` ${SPECIFIC_INSTRUCTIONS}`);
184229
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
184230
+ console.log(
184231
+ "\n We also recommend allowing your agent to run `specific docs *`"
184232
+ );
184233
+ console.log(" and `specific check *` without confirmation.");
184234
+ }
184161
184235
  }
184162
184236
 
184163
184237
  // src/commands/docs.tsx
@@ -184166,11 +184240,11 @@ import { join as join6, dirname as dirname2 } from "path";
184166
184240
  import { fileURLToPath as fileURLToPath2 } from "url";
184167
184241
  var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
184168
184242
  var docsDir = join6(__dirname2, "docs");
184169
- function docsCommand(path22) {
184170
- const docPath = resolveDocPath(path22);
184243
+ function docsCommand(path24) {
184244
+ const docPath = resolveDocPath(path24);
184171
184245
  if (!docPath) {
184172
184246
  console.error(
184173
- `Documentation not found: ${path22 || "index"}
184247
+ `Documentation not found: ${path24 || "index"}
184174
184248
 
184175
184249
  Run 'specific docs' to see available topics.`
184176
184250
  );
@@ -184179,16 +184253,16 @@ Run 'specific docs' to see available topics.`
184179
184253
  const content = readFileSync5(docPath, "utf-8");
184180
184254
  console.log(content);
184181
184255
  }
184182
- function resolveDocPath(path22) {
184183
- if (!path22) {
184256
+ function resolveDocPath(path24) {
184257
+ if (!path24) {
184184
184258
  const indexPath2 = join6(docsDir, "index.md");
184185
184259
  return existsSync5(indexPath2) ? indexPath2 : null;
184186
184260
  }
184187
- const directPath = join6(docsDir, `${path22}.md`);
184261
+ const directPath = join6(docsDir, `${path24}.md`);
184188
184262
  if (existsSync5(directPath)) {
184189
184263
  return directPath;
184190
184264
  }
184191
- const indexPath = join6(docsDir, path22, "index.md");
184265
+ const indexPath = join6(docsDir, path24, "index.md");
184192
184266
  if (existsSync5(indexPath)) {
184193
184267
  return indexPath;
184194
184268
  }
@@ -184283,7 +184357,7 @@ function parseReferenceString(str) {
184283
184357
  attribute: "port"
184284
184358
  };
184285
184359
  }
184286
- const serviceEndpointMatch = str.match(/^service\.(\w+)\.endpoint\.(\w+)\.(url|host|port)$/);
184360
+ const serviceEndpointMatch = str.match(/^service\.(\w+)\.endpoint\.(\w+)\.(url|private_url|host|port|public_url)$/);
184287
184361
  if (serviceEndpointMatch && serviceEndpointMatch[1] && serviceEndpointMatch[2] && serviceEndpointMatch[3]) {
184288
184362
  return {
184289
184363
  type: "service",
@@ -184292,7 +184366,7 @@ function parseReferenceString(str) {
184292
184366
  attribute: serviceEndpointMatch[3]
184293
184367
  };
184294
184368
  }
184295
- const serviceMatch = str.match(/^service\.(\w+)\.(url|host|port)$/);
184369
+ const serviceMatch = str.match(/^service\.(\w+)\.(url|private_url|host|port|public_url)$/);
184296
184370
  if (serviceMatch && serviceMatch[1] && serviceMatch[2]) {
184297
184371
  return {
184298
184372
  type: "service",
@@ -184397,6 +184471,17 @@ function parseBuilds(buildData) {
184397
184471
  if (dev) {
184398
184472
  build.dev = dev;
184399
184473
  }
184474
+ const env2 = parseEnv(fieldObj.env);
184475
+ if (env2) {
184476
+ for (const [key, value] of Object.entries(env2)) {
184477
+ if (typeof value === "string")
184478
+ continue;
184479
+ if (value.type === "service" && value.attribute === "public_url")
184480
+ continue;
184481
+ throw new Error(`Build "${name}" env var "${key}" uses an unsupported reference type. Build env vars only support string literals and \${service.<name>.public_url} references.`);
184482
+ }
184483
+ build.env = env2;
184484
+ }
184400
184485
  builds.push(build);
184401
184486
  }
184402
184487
  return builds;
@@ -184704,6 +184789,26 @@ function validateEndpointReferences(config) {
184704
184789
  });
184705
184790
  }
184706
184791
  }
184792
+ if (serviceRef.attribute === "public_url") {
184793
+ const endpointName = serviceRef.endpointName;
184794
+ if (endpointName) {
184795
+ const targetEndpoint = targetEndpoints.find((e) => e.name === endpointName);
184796
+ if (targetEndpoint && !targetEndpoint.public) {
184797
+ errors.push({
184798
+ service: service.name,
184799
+ message: `Service "${service.name}" references public_url of endpoint "${endpointName}" on service "${serviceRef.serviceName}" in env var "${key}", but that endpoint is not public. Add \`public = true\` to the endpoint.`
184800
+ });
184801
+ }
184802
+ } else {
184803
+ const defaultEndpoint = targetEndpoints.length === 1 ? targetEndpoints[0] : targetEndpoints.find((e) => e.name === "default");
184804
+ if (defaultEndpoint && !defaultEndpoint.public) {
184805
+ errors.push({
184806
+ service: service.name,
184807
+ message: `Service "${service.name}" references public_url of service "${serviceRef.serviceName}" in env var "${key}", but its endpoint is not public. Add \`public = true\` to the endpoint.`
184808
+ });
184809
+ }
184810
+ }
184811
+ }
184707
184812
  }
184708
184813
  if (typeof value === "object" && value.type === "endpoint") {
184709
184814
  const endpointRef = value;
@@ -184767,8 +184872,8 @@ function checkCommand() {
184767
184872
  import React6, { useState as useState5, useEffect as useEffect3, useRef } from "react";
184768
184873
  import { render as render4, Text as Text6, Box as Box6, useApp as useApp2, Static } from "ink";
184769
184874
  import Spinner4 from "ink-spinner";
184770
- import * as fs19 from "fs";
184771
- import * as path16 from "path";
184875
+ import * as fs20 from "fs";
184876
+ import * as path17 from "path";
184772
184877
 
184773
184878
  // node_modules/.pnpm/chokidar@5.0.0/node_modules/chokidar/index.js
184774
184879
  import { EventEmitter } from "node:events";
@@ -184860,7 +184965,7 @@ var ReaddirpStream = class extends Readable {
184860
184965
  this._directoryFilter = normalizeFilter(opts.directoryFilter);
184861
184966
  const statMethod = opts.lstat ? lstat : stat;
184862
184967
  if (wantBigintFsStats) {
184863
- this._stat = (path22) => statMethod(path22, { bigint: true });
184968
+ this._stat = (path24) => statMethod(path24, { bigint: true });
184864
184969
  } else {
184865
184970
  this._stat = statMethod;
184866
184971
  }
@@ -184885,8 +184990,8 @@ var ReaddirpStream = class extends Readable {
184885
184990
  const par = this.parent;
184886
184991
  const fil = par && par.files;
184887
184992
  if (fil && fil.length > 0) {
184888
- const { path: path22, depth } = par;
184889
- const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path22));
184993
+ const { path: path24, depth } = par;
184994
+ const slice = fil.splice(0, batch).map((dirent) => this._formatEntry(dirent, path24));
184890
184995
  const awaited = await Promise.all(slice);
184891
184996
  for (const entry of awaited) {
184892
184997
  if (!entry)
@@ -184926,20 +185031,20 @@ var ReaddirpStream = class extends Readable {
184926
185031
  this.reading = false;
184927
185032
  }
184928
185033
  }
184929
- async _exploreDir(path22, depth) {
185034
+ async _exploreDir(path24, depth) {
184930
185035
  let files;
184931
185036
  try {
184932
- files = await readdir(path22, this._rdOptions);
185037
+ files = await readdir(path24, this._rdOptions);
184933
185038
  } catch (error) {
184934
185039
  this._onError(error);
184935
185040
  }
184936
- return { files, depth, path: path22 };
185041
+ return { files, depth, path: path24 };
184937
185042
  }
184938
- async _formatEntry(dirent, path22) {
185043
+ async _formatEntry(dirent, path24) {
184939
185044
  let entry;
184940
185045
  const basename5 = this._isDirent ? dirent.name : dirent;
184941
185046
  try {
184942
- const fullPath = presolve(pjoin(path22, basename5));
185047
+ const fullPath = presolve(pjoin(path24, basename5));
184943
185048
  entry = { path: prelative(this._root, fullPath), fullPath, basename: basename5 };
184944
185049
  entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
184945
185050
  } catch (err) {
@@ -185339,16 +185444,16 @@ var delFromSet = (main, prop, item) => {
185339
185444
  };
185340
185445
  var isEmptySet = (val) => val instanceof Set ? val.size === 0 : !val;
185341
185446
  var FsWatchInstances = /* @__PURE__ */ new Map();
185342
- function createFsWatchInstance(path22, options2, listener, errHandler, emitRaw) {
185447
+ function createFsWatchInstance(path24, options2, listener, errHandler, emitRaw) {
185343
185448
  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));
185449
+ listener(path24);
185450
+ emitRaw(rawEvent, evPath, { watchedPath: path24 });
185451
+ if (evPath && path24 !== evPath) {
185452
+ fsWatchBroadcast(sp.resolve(path24, evPath), KEY_LISTENERS, sp.join(path24, evPath));
185348
185453
  }
185349
185454
  };
185350
185455
  try {
185351
- return fs_watch(path22, {
185456
+ return fs_watch(path24, {
185352
185457
  persistent: options2.persistent
185353
185458
  }, handleEvent);
185354
185459
  } catch (error) {
@@ -185364,12 +185469,12 @@ var fsWatchBroadcast = (fullPath, listenerType, val1, val2, val3) => {
185364
185469
  listener(val1, val2, val3);
185365
185470
  });
185366
185471
  };
185367
- var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185472
+ var setFsWatchListener = (path24, fullPath, options2, handlers) => {
185368
185473
  const { listener, errHandler, rawEmitter } = handlers;
185369
185474
  let cont = FsWatchInstances.get(fullPath);
185370
185475
  let watcher;
185371
185476
  if (!options2.persistent) {
185372
- watcher = createFsWatchInstance(path22, options2, listener, errHandler, rawEmitter);
185477
+ watcher = createFsWatchInstance(path24, options2, listener, errHandler, rawEmitter);
185373
185478
  if (!watcher)
185374
185479
  return;
185375
185480
  return watcher.close.bind(watcher);
@@ -185380,7 +185485,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185380
185485
  addAndConvert(cont, KEY_RAW, rawEmitter);
185381
185486
  } else {
185382
185487
  watcher = createFsWatchInstance(
185383
- path22,
185488
+ path24,
185384
185489
  options2,
185385
185490
  fsWatchBroadcast.bind(null, fullPath, KEY_LISTENERS),
185386
185491
  errHandler,
@@ -185395,7 +185500,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185395
185500
  cont.watcherUnusable = true;
185396
185501
  if (isWindows && error.code === "EPERM") {
185397
185502
  try {
185398
- const fd = await open2(path22, "r");
185503
+ const fd = await open2(path24, "r");
185399
185504
  await fd.close();
185400
185505
  broadcastErr(error);
185401
185506
  } catch (err) {
@@ -185426,7 +185531,7 @@ var setFsWatchListener = (path22, fullPath, options2, handlers) => {
185426
185531
  };
185427
185532
  };
185428
185533
  var FsWatchFileInstances = /* @__PURE__ */ new Map();
185429
- var setFsWatchFileListener = (path22, fullPath, options2, handlers) => {
185534
+ var setFsWatchFileListener = (path24, fullPath, options2, handlers) => {
185430
185535
  const { listener, rawEmitter } = handlers;
185431
185536
  let cont = FsWatchFileInstances.get(fullPath);
185432
185537
  const copts = cont && cont.options;
@@ -185448,7 +185553,7 @@ var setFsWatchFileListener = (path22, fullPath, options2, handlers) => {
185448
185553
  });
185449
185554
  const currmtime = curr.mtimeMs;
185450
185555
  if (curr.size !== prev.size || currmtime > prev.mtimeMs || currmtime === 0) {
185451
- foreach(cont.listeners, (listener2) => listener2(path22, curr));
185556
+ foreach(cont.listeners, (listener2) => listener2(path24, curr));
185452
185557
  }
185453
185558
  })
185454
185559
  };
@@ -185478,13 +185583,13 @@ var NodeFsHandler = class {
185478
185583
  * @param listener on fs change
185479
185584
  * @returns closer for the watcher instance
185480
185585
  */
185481
- _watchWithNodeFs(path22, listener) {
185586
+ _watchWithNodeFs(path24, listener) {
185482
185587
  const opts = this.fsw.options;
185483
- const directory = sp.dirname(path22);
185484
- const basename5 = sp.basename(path22);
185588
+ const directory = sp.dirname(path24);
185589
+ const basename5 = sp.basename(path24);
185485
185590
  const parent = this.fsw._getWatchedDir(directory);
185486
185591
  parent.add(basename5);
185487
- const absolutePath = sp.resolve(path22);
185592
+ const absolutePath = sp.resolve(path24);
185488
185593
  const options2 = {
185489
185594
  persistent: opts.persistent
185490
185595
  };
@@ -185494,12 +185599,12 @@ var NodeFsHandler = class {
185494
185599
  if (opts.usePolling) {
185495
185600
  const enableBin = opts.interval !== opts.binaryInterval;
185496
185601
  options2.interval = enableBin && isBinaryPath(basename5) ? opts.binaryInterval : opts.interval;
185497
- closer = setFsWatchFileListener(path22, absolutePath, options2, {
185602
+ closer = setFsWatchFileListener(path24, absolutePath, options2, {
185498
185603
  listener,
185499
185604
  rawEmitter: this.fsw._emitRaw
185500
185605
  });
185501
185606
  } else {
185502
- closer = setFsWatchListener(path22, absolutePath, options2, {
185607
+ closer = setFsWatchListener(path24, absolutePath, options2, {
185503
185608
  listener,
185504
185609
  errHandler: this._boundHandleError,
185505
185610
  rawEmitter: this.fsw._emitRaw
@@ -185521,7 +185626,7 @@ var NodeFsHandler = class {
185521
185626
  let prevStats = stats;
185522
185627
  if (parent.has(basename5))
185523
185628
  return;
185524
- const listener = async (path22, newStats) => {
185629
+ const listener = async (path24, newStats) => {
185525
185630
  if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file, 5))
185526
185631
  return;
185527
185632
  if (!newStats || newStats.mtimeMs === 0) {
@@ -185535,11 +185640,11 @@ var NodeFsHandler = class {
185535
185640
  this.fsw._emit(EV.CHANGE, file, newStats2);
185536
185641
  }
185537
185642
  if ((isMacos || isLinux || isFreeBSD) && prevStats.ino !== newStats2.ino) {
185538
- this.fsw._closeFile(path22);
185643
+ this.fsw._closeFile(path24);
185539
185644
  prevStats = newStats2;
185540
185645
  const closer2 = this._watchWithNodeFs(file, listener);
185541
185646
  if (closer2)
185542
- this.fsw._addPathCloser(path22, closer2);
185647
+ this.fsw._addPathCloser(path24, closer2);
185543
185648
  } else {
185544
185649
  prevStats = newStats2;
185545
185650
  }
@@ -185571,7 +185676,7 @@ var NodeFsHandler = class {
185571
185676
  * @param item basename of this item
185572
185677
  * @returns true if no more processing is needed for this entry.
185573
185678
  */
185574
- async _handleSymlink(entry, directory, path22, item) {
185679
+ async _handleSymlink(entry, directory, path24, item) {
185575
185680
  if (this.fsw.closed) {
185576
185681
  return;
185577
185682
  }
@@ -185581,7 +185686,7 @@ var NodeFsHandler = class {
185581
185686
  this.fsw._incrReadyCount();
185582
185687
  let linkPath;
185583
185688
  try {
185584
- linkPath = await fsrealpath(path22);
185689
+ linkPath = await fsrealpath(path24);
185585
185690
  } catch (e) {
185586
185691
  this.fsw._emitReady();
185587
185692
  return true;
@@ -185591,12 +185696,12 @@ var NodeFsHandler = class {
185591
185696
  if (dir.has(item)) {
185592
185697
  if (this.fsw._symlinkPaths.get(full) !== linkPath) {
185593
185698
  this.fsw._symlinkPaths.set(full, linkPath);
185594
- this.fsw._emit(EV.CHANGE, path22, entry.stats);
185699
+ this.fsw._emit(EV.CHANGE, path24, entry.stats);
185595
185700
  }
185596
185701
  } else {
185597
185702
  dir.add(item);
185598
185703
  this.fsw._symlinkPaths.set(full, linkPath);
185599
- this.fsw._emit(EV.ADD, path22, entry.stats);
185704
+ this.fsw._emit(EV.ADD, path24, entry.stats);
185600
185705
  }
185601
185706
  this.fsw._emitReady();
185602
185707
  return true;
@@ -185626,9 +185731,9 @@ var NodeFsHandler = class {
185626
185731
  return;
185627
185732
  }
185628
185733
  const item = entry.path;
185629
- let path22 = sp.join(directory, item);
185734
+ let path24 = sp.join(directory, item);
185630
185735
  current.add(item);
185631
- if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path22, item)) {
185736
+ if (entry.stats.isSymbolicLink() && await this._handleSymlink(entry, directory, path24, item)) {
185632
185737
  return;
185633
185738
  }
185634
185739
  if (this.fsw.closed) {
@@ -185637,8 +185742,8 @@ var NodeFsHandler = class {
185637
185742
  }
185638
185743
  if (item === target || !target && !previous.has(item)) {
185639
185744
  this.fsw._incrReadyCount();
185640
- path22 = sp.join(dir, sp.relative(dir, path22));
185641
- this._addToNodeFs(path22, initialAdd, wh, depth + 1);
185745
+ path24 = sp.join(dir, sp.relative(dir, path24));
185746
+ this._addToNodeFs(path24, initialAdd, wh, depth + 1);
185642
185747
  }
185643
185748
  }).on(EV.ERROR, this._boundHandleError);
185644
185749
  return new Promise((resolve7, reject) => {
@@ -185707,13 +185812,13 @@ var NodeFsHandler = class {
185707
185812
  * @param depth Child path actually targeted for watch
185708
185813
  * @param target Child path actually targeted for watch
185709
185814
  */
185710
- async _addToNodeFs(path22, initialAdd, priorWh, depth, target) {
185815
+ async _addToNodeFs(path24, initialAdd, priorWh, depth, target) {
185711
185816
  const ready = this.fsw._emitReady;
185712
- if (this.fsw._isIgnored(path22) || this.fsw.closed) {
185817
+ if (this.fsw._isIgnored(path24) || this.fsw.closed) {
185713
185818
  ready();
185714
185819
  return false;
185715
185820
  }
185716
- const wh = this.fsw._getWatchHelpers(path22);
185821
+ const wh = this.fsw._getWatchHelpers(path24);
185717
185822
  if (priorWh) {
185718
185823
  wh.filterPath = (entry) => priorWh.filterPath(entry);
185719
185824
  wh.filterDir = (entry) => priorWh.filterDir(entry);
@@ -185729,8 +185834,8 @@ var NodeFsHandler = class {
185729
185834
  const follow = this.fsw.options.followSymlinks;
185730
185835
  let closer;
185731
185836
  if (stats.isDirectory()) {
185732
- const absPath = sp.resolve(path22);
185733
- const targetPath = follow ? await fsrealpath(path22) : path22;
185837
+ const absPath = sp.resolve(path24);
185838
+ const targetPath = follow ? await fsrealpath(path24) : path24;
185734
185839
  if (this.fsw.closed)
185735
185840
  return;
185736
185841
  closer = await this._handleDir(wh.watchPath, stats, initialAdd, depth, target, wh, targetPath);
@@ -185740,29 +185845,29 @@ var NodeFsHandler = class {
185740
185845
  this.fsw._symlinkPaths.set(absPath, targetPath);
185741
185846
  }
185742
185847
  } else if (stats.isSymbolicLink()) {
185743
- const targetPath = follow ? await fsrealpath(path22) : path22;
185848
+ const targetPath = follow ? await fsrealpath(path24) : path24;
185744
185849
  if (this.fsw.closed)
185745
185850
  return;
185746
185851
  const parent = sp.dirname(wh.watchPath);
185747
185852
  this.fsw._getWatchedDir(parent).add(wh.watchPath);
185748
185853
  this.fsw._emit(EV.ADD, wh.watchPath, stats);
185749
- closer = await this._handleDir(parent, stats, initialAdd, depth, path22, wh, targetPath);
185854
+ closer = await this._handleDir(parent, stats, initialAdd, depth, path24, wh, targetPath);
185750
185855
  if (this.fsw.closed)
185751
185856
  return;
185752
185857
  if (targetPath !== void 0) {
185753
- this.fsw._symlinkPaths.set(sp.resolve(path22), targetPath);
185858
+ this.fsw._symlinkPaths.set(sp.resolve(path24), targetPath);
185754
185859
  }
185755
185860
  } else {
185756
185861
  closer = this._handleFile(wh.watchPath, stats, initialAdd);
185757
185862
  }
185758
185863
  ready();
185759
185864
  if (closer)
185760
- this.fsw._addPathCloser(path22, closer);
185865
+ this.fsw._addPathCloser(path24, closer);
185761
185866
  return false;
185762
185867
  } catch (error) {
185763
185868
  if (this.fsw._handleError(error)) {
185764
185869
  ready();
185765
- return path22;
185870
+ return path24;
185766
185871
  }
185767
185872
  }
185768
185873
  }
@@ -185805,24 +185910,24 @@ function createPattern(matcher) {
185805
185910
  }
185806
185911
  return () => false;
185807
185912
  }
185808
- function normalizePath(path22) {
185809
- if (typeof path22 !== "string")
185913
+ function normalizePath(path24) {
185914
+ if (typeof path24 !== "string")
185810
185915
  throw new Error("string expected");
185811
- path22 = sp2.normalize(path22);
185812
- path22 = path22.replace(/\\/g, "/");
185916
+ path24 = sp2.normalize(path24);
185917
+ path24 = path24.replace(/\\/g, "/");
185813
185918
  let prepend = false;
185814
- if (path22.startsWith("//"))
185919
+ if (path24.startsWith("//"))
185815
185920
  prepend = true;
185816
- path22 = path22.replace(DOUBLE_SLASH_RE, "/");
185921
+ path24 = path24.replace(DOUBLE_SLASH_RE, "/");
185817
185922
  if (prepend)
185818
- path22 = "/" + path22;
185819
- return path22;
185923
+ path24 = "/" + path24;
185924
+ return path24;
185820
185925
  }
185821
185926
  function matchPatterns(patterns, testString, stats) {
185822
- const path22 = normalizePath(testString);
185927
+ const path24 = normalizePath(testString);
185823
185928
  for (let index = 0; index < patterns.length; index++) {
185824
185929
  const pattern = patterns[index];
185825
- if (pattern(path22, stats)) {
185930
+ if (pattern(path24, stats)) {
185826
185931
  return true;
185827
185932
  }
185828
185933
  }
@@ -185860,19 +185965,19 @@ var toUnix = (string) => {
185860
185965
  }
185861
185966
  return str;
185862
185967
  };
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));
185968
+ var normalizePathToUnix = (path24) => toUnix(sp2.normalize(toUnix(path24)));
185969
+ var normalizeIgnored = (cwd = "") => (path24) => {
185970
+ if (typeof path24 === "string") {
185971
+ return normalizePathToUnix(sp2.isAbsolute(path24) ? path24 : sp2.join(cwd, path24));
185867
185972
  } else {
185868
- return path22;
185973
+ return path24;
185869
185974
  }
185870
185975
  };
185871
- var getAbsolutePath = (path22, cwd) => {
185872
- if (sp2.isAbsolute(path22)) {
185873
- return path22;
185976
+ var getAbsolutePath = (path24, cwd) => {
185977
+ if (sp2.isAbsolute(path24)) {
185978
+ return path24;
185874
185979
  }
185875
- return sp2.join(cwd, path22);
185980
+ return sp2.join(cwd, path24);
185876
185981
  };
185877
185982
  var EMPTY_SET = Object.freeze(/* @__PURE__ */ new Set());
185878
185983
  var DirEntry = class {
@@ -185937,10 +186042,10 @@ var WatchHelper = class {
185937
186042
  dirParts;
185938
186043
  followSymlinks;
185939
186044
  statMethod;
185940
- constructor(path22, follow, fsw) {
186045
+ constructor(path24, follow, fsw) {
185941
186046
  this.fsw = fsw;
185942
- const watchPath = path22;
185943
- this.path = path22 = path22.replace(REPLACER_RE, "");
186047
+ const watchPath = path24;
186048
+ this.path = path24 = path24.replace(REPLACER_RE, "");
185944
186049
  this.watchPath = watchPath;
185945
186050
  this.fullWatchPath = sp2.resolve(watchPath);
185946
186051
  this.dirParts = [];
@@ -186080,20 +186185,20 @@ var FSWatcher = class extends EventEmitter {
186080
186185
  this._closePromise = void 0;
186081
186186
  let paths = unifyPaths(paths_);
186082
186187
  if (cwd) {
186083
- paths = paths.map((path22) => {
186084
- const absPath = getAbsolutePath(path22, cwd);
186188
+ paths = paths.map((path24) => {
186189
+ const absPath = getAbsolutePath(path24, cwd);
186085
186190
  return absPath;
186086
186191
  });
186087
186192
  }
186088
- paths.forEach((path22) => {
186089
- this._removeIgnoredPath(path22);
186193
+ paths.forEach((path24) => {
186194
+ this._removeIgnoredPath(path24);
186090
186195
  });
186091
186196
  this._userIgnored = void 0;
186092
186197
  if (!this._readyCount)
186093
186198
  this._readyCount = 0;
186094
186199
  this._readyCount += paths.length;
186095
- Promise.all(paths.map(async (path22) => {
186096
- const res = await this._nodeFsHandler._addToNodeFs(path22, !_internal, void 0, 0, _origAdd);
186200
+ Promise.all(paths.map(async (path24) => {
186201
+ const res = await this._nodeFsHandler._addToNodeFs(path24, !_internal, void 0, 0, _origAdd);
186097
186202
  if (res)
186098
186203
  this._emitReady();
186099
186204
  return res;
@@ -186115,17 +186220,17 @@ var FSWatcher = class extends EventEmitter {
186115
186220
  return this;
186116
186221
  const paths = unifyPaths(paths_);
186117
186222
  const { cwd } = this.options;
186118
- paths.forEach((path22) => {
186119
- if (!sp2.isAbsolute(path22) && !this._closers.has(path22)) {
186223
+ paths.forEach((path24) => {
186224
+ if (!sp2.isAbsolute(path24) && !this._closers.has(path24)) {
186120
186225
  if (cwd)
186121
- path22 = sp2.join(cwd, path22);
186122
- path22 = sp2.resolve(path22);
186226
+ path24 = sp2.join(cwd, path24);
186227
+ path24 = sp2.resolve(path24);
186123
186228
  }
186124
- this._closePath(path22);
186125
- this._addIgnoredPath(path22);
186126
- if (this._watched.has(path22)) {
186229
+ this._closePath(path24);
186230
+ this._addIgnoredPath(path24);
186231
+ if (this._watched.has(path24)) {
186127
186232
  this._addIgnoredPath({
186128
- path: path22,
186233
+ path: path24,
186129
186234
  recursive: true
186130
186235
  });
186131
186236
  }
@@ -186189,38 +186294,38 @@ var FSWatcher = class extends EventEmitter {
186189
186294
  * @param stats arguments to be passed with event
186190
186295
  * @returns the error if defined, otherwise the value of the FSWatcher instance's `closed` flag
186191
186296
  */
186192
- async _emit(event, path22, stats) {
186297
+ async _emit(event, path24, stats) {
186193
186298
  if (this.closed)
186194
186299
  return;
186195
186300
  const opts = this.options;
186196
186301
  if (isWindows)
186197
- path22 = sp2.normalize(path22);
186302
+ path24 = sp2.normalize(path24);
186198
186303
  if (opts.cwd)
186199
- path22 = sp2.relative(opts.cwd, path22);
186200
- const args = [path22];
186304
+ path24 = sp2.relative(opts.cwd, path24);
186305
+ const args = [path24];
186201
186306
  if (stats != null)
186202
186307
  args.push(stats);
186203
186308
  const awf = opts.awaitWriteFinish;
186204
186309
  let pw;
186205
- if (awf && (pw = this._pendingWrites.get(path22))) {
186310
+ if (awf && (pw = this._pendingWrites.get(path24))) {
186206
186311
  pw.lastChange = /* @__PURE__ */ new Date();
186207
186312
  return this;
186208
186313
  }
186209
186314
  if (opts.atomic) {
186210
186315
  if (event === EVENTS.UNLINK) {
186211
- this._pendingUnlinks.set(path22, [event, ...args]);
186316
+ this._pendingUnlinks.set(path24, [event, ...args]);
186212
186317
  setTimeout(() => {
186213
- this._pendingUnlinks.forEach((entry, path23) => {
186318
+ this._pendingUnlinks.forEach((entry, path25) => {
186214
186319
  this.emit(...entry);
186215
186320
  this.emit(EVENTS.ALL, ...entry);
186216
- this._pendingUnlinks.delete(path23);
186321
+ this._pendingUnlinks.delete(path25);
186217
186322
  });
186218
186323
  }, typeof opts.atomic === "number" ? opts.atomic : 100);
186219
186324
  return this;
186220
186325
  }
186221
- if (event === EVENTS.ADD && this._pendingUnlinks.has(path22)) {
186326
+ if (event === EVENTS.ADD && this._pendingUnlinks.has(path24)) {
186222
186327
  event = EVENTS.CHANGE;
186223
- this._pendingUnlinks.delete(path22);
186328
+ this._pendingUnlinks.delete(path24);
186224
186329
  }
186225
186330
  }
186226
186331
  if (awf && (event === EVENTS.ADD || event === EVENTS.CHANGE) && this._readyEmitted) {
@@ -186238,16 +186343,16 @@ var FSWatcher = class extends EventEmitter {
186238
186343
  this.emitWithAll(event, args);
186239
186344
  }
186240
186345
  };
186241
- this._awaitWriteFinish(path22, awf.stabilityThreshold, event, awfEmit);
186346
+ this._awaitWriteFinish(path24, awf.stabilityThreshold, event, awfEmit);
186242
186347
  return this;
186243
186348
  }
186244
186349
  if (event === EVENTS.CHANGE) {
186245
- const isThrottled = !this._throttle(EVENTS.CHANGE, path22, 50);
186350
+ const isThrottled = !this._throttle(EVENTS.CHANGE, path24, 50);
186246
186351
  if (isThrottled)
186247
186352
  return this;
186248
186353
  }
186249
186354
  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;
186355
+ const fullPath = opts.cwd ? sp2.join(opts.cwd, path24) : path24;
186251
186356
  let stats2;
186252
186357
  try {
186253
186358
  stats2 = await stat3(fullPath);
@@ -186278,23 +186383,23 @@ var FSWatcher = class extends EventEmitter {
186278
186383
  * @param timeout duration of time to suppress duplicate actions
186279
186384
  * @returns tracking object or false if action should be suppressed
186280
186385
  */
186281
- _throttle(actionType, path22, timeout) {
186386
+ _throttle(actionType, path24, timeout) {
186282
186387
  if (!this._throttled.has(actionType)) {
186283
186388
  this._throttled.set(actionType, /* @__PURE__ */ new Map());
186284
186389
  }
186285
186390
  const action = this._throttled.get(actionType);
186286
186391
  if (!action)
186287
186392
  throw new Error("invalid throttle");
186288
- const actionPath = action.get(path22);
186393
+ const actionPath = action.get(path24);
186289
186394
  if (actionPath) {
186290
186395
  actionPath.count++;
186291
186396
  return false;
186292
186397
  }
186293
186398
  let timeoutObject;
186294
186399
  const clear = () => {
186295
- const item = action.get(path22);
186400
+ const item = action.get(path24);
186296
186401
  const count = item ? item.count : 0;
186297
- action.delete(path22);
186402
+ action.delete(path24);
186298
186403
  clearTimeout(timeoutObject);
186299
186404
  if (item)
186300
186405
  clearTimeout(item.timeoutObject);
@@ -186302,7 +186407,7 @@ var FSWatcher = class extends EventEmitter {
186302
186407
  };
186303
186408
  timeoutObject = setTimeout(clear, timeout);
186304
186409
  const thr = { timeoutObject, clear, count: 0 };
186305
- action.set(path22, thr);
186410
+ action.set(path24, thr);
186306
186411
  return thr;
186307
186412
  }
186308
186413
  _incrReadyCount() {
@@ -186316,44 +186421,44 @@ var FSWatcher = class extends EventEmitter {
186316
186421
  * @param event
186317
186422
  * @param awfEmit Callback to be called when ready for event to be emitted.
186318
186423
  */
186319
- _awaitWriteFinish(path22, threshold, event, awfEmit) {
186424
+ _awaitWriteFinish(path24, threshold, event, awfEmit) {
186320
186425
  const awf = this.options.awaitWriteFinish;
186321
186426
  if (typeof awf !== "object")
186322
186427
  return;
186323
186428
  const pollInterval = awf.pollInterval;
186324
186429
  let timeoutHandler;
186325
- let fullPath = path22;
186326
- if (this.options.cwd && !sp2.isAbsolute(path22)) {
186327
- fullPath = sp2.join(this.options.cwd, path22);
186430
+ let fullPath = path24;
186431
+ if (this.options.cwd && !sp2.isAbsolute(path24)) {
186432
+ fullPath = sp2.join(this.options.cwd, path24);
186328
186433
  }
186329
186434
  const now = /* @__PURE__ */ new Date();
186330
186435
  const writes = this._pendingWrites;
186331
186436
  function awaitWriteFinishFn(prevStat) {
186332
186437
  statcb(fullPath, (err, curStat) => {
186333
- if (err || !writes.has(path22)) {
186438
+ if (err || !writes.has(path24)) {
186334
186439
  if (err && err.code !== "ENOENT")
186335
186440
  awfEmit(err);
186336
186441
  return;
186337
186442
  }
186338
186443
  const now2 = Number(/* @__PURE__ */ new Date());
186339
186444
  if (prevStat && curStat.size !== prevStat.size) {
186340
- writes.get(path22).lastChange = now2;
186445
+ writes.get(path24).lastChange = now2;
186341
186446
  }
186342
- const pw = writes.get(path22);
186447
+ const pw = writes.get(path24);
186343
186448
  const df = now2 - pw.lastChange;
186344
186449
  if (df >= threshold) {
186345
- writes.delete(path22);
186450
+ writes.delete(path24);
186346
186451
  awfEmit(void 0, curStat);
186347
186452
  } else {
186348
186453
  timeoutHandler = setTimeout(awaitWriteFinishFn, pollInterval, curStat);
186349
186454
  }
186350
186455
  });
186351
186456
  }
186352
- if (!writes.has(path22)) {
186353
- writes.set(path22, {
186457
+ if (!writes.has(path24)) {
186458
+ writes.set(path24, {
186354
186459
  lastChange: now,
186355
186460
  cancelWait: () => {
186356
- writes.delete(path22);
186461
+ writes.delete(path24);
186357
186462
  clearTimeout(timeoutHandler);
186358
186463
  return event;
186359
186464
  }
@@ -186364,8 +186469,8 @@ var FSWatcher = class extends EventEmitter {
186364
186469
  /**
186365
186470
  * Determines whether user has asked to ignore this path.
186366
186471
  */
186367
- _isIgnored(path22, stats) {
186368
- if (this.options.atomic && DOT_RE.test(path22))
186472
+ _isIgnored(path24, stats) {
186473
+ if (this.options.atomic && DOT_RE.test(path24))
186369
186474
  return true;
186370
186475
  if (!this._userIgnored) {
186371
186476
  const { cwd } = this.options;
@@ -186375,17 +186480,17 @@ var FSWatcher = class extends EventEmitter {
186375
186480
  const list = [...ignoredPaths.map(normalizeIgnored(cwd)), ...ignored];
186376
186481
  this._userIgnored = anymatch(list, void 0);
186377
186482
  }
186378
- return this._userIgnored(path22, stats);
186483
+ return this._userIgnored(path24, stats);
186379
186484
  }
186380
- _isntIgnored(path22, stat4) {
186381
- return !this._isIgnored(path22, stat4);
186485
+ _isntIgnored(path24, stat4) {
186486
+ return !this._isIgnored(path24, stat4);
186382
186487
  }
186383
186488
  /**
186384
186489
  * Provides a set of common helpers and properties relating to symlink handling.
186385
186490
  * @param path file or directory pattern being watched
186386
186491
  */
186387
- _getWatchHelpers(path22) {
186388
- return new WatchHelper(path22, this.options.followSymlinks, this);
186492
+ _getWatchHelpers(path24) {
186493
+ return new WatchHelper(path24, this.options.followSymlinks, this);
186389
186494
  }
186390
186495
  // Directory helpers
186391
186496
  // -----------------
@@ -186417,63 +186522,63 @@ var FSWatcher = class extends EventEmitter {
186417
186522
  * @param item base path of item/directory
186418
186523
  */
186419
186524
  _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))
186525
+ const path24 = sp2.join(directory, item);
186526
+ const fullPath = sp2.resolve(path24);
186527
+ isDirectory = isDirectory != null ? isDirectory : this._watched.has(path24) || this._watched.has(fullPath);
186528
+ if (!this._throttle("remove", path24, 100))
186424
186529
  return;
186425
186530
  if (!isDirectory && this._watched.size === 1) {
186426
186531
  this.add(directory, item, true);
186427
186532
  }
186428
- const wp = this._getWatchedDir(path22);
186533
+ const wp = this._getWatchedDir(path24);
186429
186534
  const nestedDirectoryChildren = wp.getChildren();
186430
- nestedDirectoryChildren.forEach((nested) => this._remove(path22, nested));
186535
+ nestedDirectoryChildren.forEach((nested) => this._remove(path24, nested));
186431
186536
  const parent = this._getWatchedDir(directory);
186432
186537
  const wasTracked = parent.has(item);
186433
186538
  parent.remove(item);
186434
186539
  if (this._symlinkPaths.has(fullPath)) {
186435
186540
  this._symlinkPaths.delete(fullPath);
186436
186541
  }
186437
- let relPath = path22;
186542
+ let relPath = path24;
186438
186543
  if (this.options.cwd)
186439
- relPath = sp2.relative(this.options.cwd, path22);
186544
+ relPath = sp2.relative(this.options.cwd, path24);
186440
186545
  if (this.options.awaitWriteFinish && this._pendingWrites.has(relPath)) {
186441
186546
  const event = this._pendingWrites.get(relPath).cancelWait();
186442
186547
  if (event === EVENTS.ADD)
186443
186548
  return;
186444
186549
  }
186445
- this._watched.delete(path22);
186550
+ this._watched.delete(path24);
186446
186551
  this._watched.delete(fullPath);
186447
186552
  const eventName = isDirectory ? EVENTS.UNLINK_DIR : EVENTS.UNLINK;
186448
- if (wasTracked && !this._isIgnored(path22))
186449
- this._emit(eventName, path22);
186450
- this._closePath(path22);
186553
+ if (wasTracked && !this._isIgnored(path24))
186554
+ this._emit(eventName, path24);
186555
+ this._closePath(path24);
186451
186556
  }
186452
186557
  /**
186453
186558
  * Closes all watchers for a path
186454
186559
  */
186455
- _closePath(path22) {
186456
- this._closeFile(path22);
186457
- const dir = sp2.dirname(path22);
186458
- this._getWatchedDir(dir).remove(sp2.basename(path22));
186560
+ _closePath(path24) {
186561
+ this._closeFile(path24);
186562
+ const dir = sp2.dirname(path24);
186563
+ this._getWatchedDir(dir).remove(sp2.basename(path24));
186459
186564
  }
186460
186565
  /**
186461
186566
  * Closes only file-specific watchers
186462
186567
  */
186463
- _closeFile(path22) {
186464
- const closers = this._closers.get(path22);
186568
+ _closeFile(path24) {
186569
+ const closers = this._closers.get(path24);
186465
186570
  if (!closers)
186466
186571
  return;
186467
186572
  closers.forEach((closer) => closer());
186468
- this._closers.delete(path22);
186573
+ this._closers.delete(path24);
186469
186574
  }
186470
- _addPathCloser(path22, closer) {
186575
+ _addPathCloser(path24, closer) {
186471
186576
  if (!closer)
186472
186577
  return;
186473
- let list = this._closers.get(path22);
186578
+ let list = this._closers.get(path24);
186474
186579
  if (!list) {
186475
186580
  list = [];
186476
- this._closers.set(path22, list);
186581
+ this._closers.set(path24, list);
186477
186582
  }
186478
186583
  list.push(closer);
186479
186584
  }
@@ -186591,7 +186696,6 @@ var StablePortAllocator = class {
186591
186696
  import * as fs14 from "fs";
186592
186697
  import * as path10 from "path";
186593
186698
  import * as net from "net";
186594
- import * as os7 from "os";
186595
186699
  import { spawn } from "child_process";
186596
186700
 
186597
186701
  // src/lib/bin/types.ts
@@ -186634,15 +186738,27 @@ import * as path9 from "path";
186634
186738
  import * as os6 from "os";
186635
186739
  import { createReadStream } from "fs";
186636
186740
  import { createTarExtractor, extractTo } from "tar-vern";
186741
+ function getLibraryEnv(binary) {
186742
+ if (!binary.libraryPath) {
186743
+ return {};
186744
+ }
186745
+ const platform4 = os6.platform();
186746
+ if (platform4 === "darwin") {
186747
+ return { DYLD_LIBRARY_PATH: binary.libraryPath };
186748
+ } else if (platform4 === "linux") {
186749
+ return { LD_LIBRARY_PATH: binary.libraryPath };
186750
+ }
186751
+ return {};
186752
+ }
186637
186753
  function getBinBaseDir() {
186638
186754
  return path9.join(os6.homedir(), ".specific", "bin");
186639
186755
  }
186640
186756
  function getPlatformInfo() {
186641
- const platform5 = os6.platform();
186757
+ const platform4 = os6.platform();
186642
186758
  const arch3 = os6.arch();
186643
- if (platform5 !== "darwin" && platform5 !== "linux") {
186759
+ if (platform4 !== "darwin" && platform4 !== "linux") {
186644
186760
  throw new Error(
186645
- `Unsupported platform: ${platform5}. Only macOS and Linux are supported.`
186761
+ `Unsupported platform: ${platform4}. Only macOS and Linux are supported.`
186646
186762
  );
186647
186763
  }
186648
186764
  const archStr = arch3;
@@ -186656,7 +186772,7 @@ function getPlatformInfo() {
186656
186772
  `Unsupported architecture: ${arch3}. Only x64 and arm64 are supported.`
186657
186773
  );
186658
186774
  }
186659
- return { platform: platform5, arch: mappedArch };
186775
+ return { platform: platform4, arch: mappedArch };
186660
186776
  }
186661
186777
  function getBinaryDir(definition, version, platformInfo) {
186662
186778
  return path9.join(
@@ -186832,7 +186948,7 @@ var postgresBinary = {
186832
186948
  // Archive contains bin/ and lib/ directories at root
186833
186949
  stripComponents: 0,
186834
186950
  // Core PostgreSQL executables (in bin/ directory)
186835
- executables: ["bin/postgres", "bin/initdb"],
186951
+ executables: ["bin/postgres", "bin/initdb", "bin/psql"],
186836
186952
  // Library directory relative to root (for setting LD_LIBRARY_PATH)
186837
186953
  libraryDir: "lib"
186838
186954
  };
@@ -186904,18 +187020,6 @@ var drizzleGatewayBinary = {
186904
187020
  };
186905
187021
 
186906
187022
  // src/lib/dev/database-manager.ts
186907
- function getLibraryEnv(binary) {
186908
- if (!binary.libraryPath) {
186909
- return {};
186910
- }
186911
- const platform5 = os7.platform();
186912
- if (platform5 === "darwin") {
186913
- return { DYLD_LIBRARY_PATH: binary.libraryPath };
186914
- } else if (platform5 === "linux") {
186915
- return { LD_LIBRARY_PATH: binary.libraryPath };
186916
- }
186917
- return {};
186918
- }
186919
187023
  async function startPostgres(pg, port, dataDir, onProgress) {
186920
187024
  const binary = await ensureBinary(postgresBinary, void 0, onProgress);
186921
187025
  const dbDataPath = path10.join(process.cwd(), dataDir, pg.name);
@@ -187271,7 +187375,7 @@ config {
187271
187375
  this.name = "MissingConfigError";
187272
187376
  }
187273
187377
  };
187274
- function resolveEnvValue(value, resources, secrets, configs, servicePort, serviceEndpoints, currentServicePorts) {
187378
+ function resolveEnvValue(value, resources, secrets, configs, servicePort, serviceEndpoints, currentServicePorts, publicUrls) {
187275
187379
  if (typeof value === "string") {
187276
187380
  return value;
187277
187381
  }
@@ -187312,11 +187416,25 @@ function resolveEnvValue(value, resources, secrets, configs, servicePort, servic
187312
187416
  }
187313
187417
  switch (serviceRef.attribute) {
187314
187418
  case "url":
187419
+ case "private_url":
187315
187420
  return `localhost:${endpoint.port}`;
187316
187421
  case "host":
187317
187422
  return "localhost";
187318
187423
  case "port":
187319
187424
  return String(endpoint.port);
187425
+ case "public_url": {
187426
+ if (!publicUrls) {
187427
+ throw new Error("public_url reference used but no public URL map provided");
187428
+ }
187429
+ const k8sName = endpointName === "default" ? serviceRef.serviceName : `${serviceRef.serviceName}-${endpointName}`;
187430
+ const publicUrl = publicUrls.get(k8sName);
187431
+ if (!publicUrl) {
187432
+ throw new Error(
187433
+ `No public URL found for service "${serviceRef.serviceName}" endpoint "${endpointName}"`
187434
+ );
187435
+ }
187436
+ return publicUrl;
187437
+ }
187320
187438
  default:
187321
187439
  throw new Error(`Unknown service attribute: ${serviceRef.attribute}`);
187322
187440
  }
@@ -187421,7 +187539,7 @@ function resolveEnvValue(value, resources, secrets, configs, servicePort, servic
187421
187539
  throw new Error(`Unknown reference type`);
187422
187540
  }
187423
187541
  }
187424
- function resolveEnv(env2, resources, secrets, configs, servicePort, serviceEndpoints, currentServicePorts) {
187542
+ function resolveEnv(env2, resources, secrets, configs, servicePort, serviceEndpoints, currentServicePorts, publicUrls) {
187425
187543
  if (!env2) {
187426
187544
  return {};
187427
187545
  }
@@ -187434,7 +187552,8 @@ function resolveEnv(env2, resources, secrets, configs, servicePort, serviceEndpo
187434
187552
  configs,
187435
187553
  servicePort,
187436
187554
  serviceEndpoints,
187437
- currentServicePorts
187555
+ currentServicePorts,
187556
+ publicUrls
187438
187557
  );
187439
187558
  }
187440
187559
  return resolved;
@@ -187458,7 +187577,7 @@ function resolveEnvForExec(env2, resources, secrets, configs) {
187458
187577
  }
187459
187578
 
187460
187579
  // src/lib/dev/service-runner.ts
187461
- function startService(service, resources, secrets, configs, endpointPorts, serviceEndpoints, onLog) {
187580
+ function startService(service, resources, secrets, configs, endpointPorts, serviceEndpoints, onLog, publicUrls) {
187462
187581
  const command = service.dev?.command ?? service.command;
187463
187582
  if (!command) {
187464
187583
  throw new Error(`Service "${service.name}" has no command`);
@@ -187475,7 +187594,8 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
187475
187594
  configs,
187476
187595
  defaultPort,
187477
187596
  serviceEndpoints,
187478
- endpointPorts
187597
+ endpointPorts,
187598
+ publicUrls
187479
187599
  );
187480
187600
  const child = spawn2(command, {
187481
187601
  shell: true,
@@ -188620,10 +188740,107 @@ function watchConfigFile(configPath, debounceMs, onChange) {
188620
188740
  };
188621
188741
  }
188622
188742
 
188623
- // src/lib/dev/proxy-registry.ts
188743
+ // src/lib/dev/subdomain-generator.ts
188624
188744
  import * as fs18 from "fs";
188625
188745
  import * as path15 from "path";
188626
- import * as os8 from "os";
188746
+ import { generateSlug } from "random-word-slugs";
188747
+ var StableSubdomainAllocator = class {
188748
+ tunnelsDir;
188749
+ tunnelsFilePath;
188750
+ baseSlug = null;
188751
+ constructor(projectRoot, key = "default") {
188752
+ this.tunnelsDir = path15.join(projectRoot, ".specific", "keys", key);
188753
+ this.tunnelsFilePath = path15.join(this.tunnelsDir, "tunnels.json");
188754
+ this.loadTunnels();
188755
+ }
188756
+ loadTunnels() {
188757
+ if (!fs18.existsSync(this.tunnelsFilePath)) {
188758
+ return;
188759
+ }
188760
+ try {
188761
+ const content = fs18.readFileSync(this.tunnelsFilePath, "utf-8");
188762
+ const data = JSON.parse(content);
188763
+ if (data.version === 1 && data.baseSlug) {
188764
+ this.baseSlug = data.baseSlug;
188765
+ }
188766
+ } catch {
188767
+ this.baseSlug = null;
188768
+ }
188769
+ }
188770
+ saveTunnels() {
188771
+ if (!fs18.existsSync(this.tunnelsDir)) {
188772
+ fs18.mkdirSync(this.tunnelsDir, { recursive: true });
188773
+ }
188774
+ const data = {
188775
+ version: 1,
188776
+ baseSlug: this.baseSlug
188777
+ };
188778
+ fs18.writeFileSync(this.tunnelsFilePath, JSON.stringify(data, null, 2));
188779
+ }
188780
+ generateBaseSlug() {
188781
+ return generateSlug(2, {
188782
+ format: "kebab",
188783
+ partsOfSpeech: ["adjective", "noun"],
188784
+ categories: {
188785
+ adjective: ["color", "appearance"],
188786
+ noun: ["animals"]
188787
+ }
188788
+ });
188789
+ }
188790
+ /**
188791
+ * Get the base slug, generating one if needed.
188792
+ */
188793
+ getBaseSlug() {
188794
+ if (!this.baseSlug) {
188795
+ this.baseSlug = this.generateBaseSlug();
188796
+ this.saveTunnels();
188797
+ }
188798
+ return this.baseSlug;
188799
+ }
188800
+ /**
188801
+ * Allocate a subdomain for a service.
188802
+ * If multipleServices is true, appends the service name to the base slug.
188803
+ */
188804
+ allocate(serviceName, multipleServices) {
188805
+ const baseSlug = this.getBaseSlug();
188806
+ if (multipleServices) {
188807
+ return `${baseSlug}-${serviceName}`;
188808
+ }
188809
+ return baseSlug;
188810
+ }
188811
+ };
188812
+
188813
+ // src/lib/dev/tunnel-manager.ts
188814
+ import localtunnel from "localtunnel";
188815
+ var TUNNEL_HOST = "https://tunnel.spcf.app";
188816
+ async function startTunnel(serviceName, endpointName, port, subdomain, callbacks) {
188817
+ const tunnel = await localtunnel({
188818
+ port,
188819
+ subdomain,
188820
+ host: TUNNEL_HOST
188821
+ });
188822
+ tunnel.on("error", (err) => {
188823
+ callbacks?.onError?.(serviceName, endpointName, err);
188824
+ });
188825
+ tunnel.on("close", () => {
188826
+ callbacks?.onClose?.(serviceName, endpointName);
188827
+ });
188828
+ return {
188829
+ serviceName,
188830
+ endpointName,
188831
+ localPort: port,
188832
+ url: tunnel.url,
188833
+ subdomain,
188834
+ stop: async () => {
188835
+ tunnel.close();
188836
+ }
188837
+ };
188838
+ }
188839
+
188840
+ // src/lib/dev/proxy-registry.ts
188841
+ import * as fs19 from "fs";
188842
+ import * as path16 from "path";
188843
+ import * as os7 from "os";
188627
188844
  import * as net4 from "net";
188628
188845
  var ProxyRegistryManager = class {
188629
188846
  proxyDir;
@@ -188633,14 +188850,14 @@ var ProxyRegistryManager = class {
188633
188850
  isOwner = false;
188634
188851
  registryWatcher = null;
188635
188852
  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");
188853
+ this.proxyDir = path16.join(os7.homedir(), ".specific", "proxy");
188854
+ this.ownerPath = path16.join(this.proxyDir, "owner.json");
188855
+ this.registryPath = path16.join(this.proxyDir, "registry.json");
188856
+ this.lockPath = path16.join(this.proxyDir, "registry.lock");
188640
188857
  }
188641
188858
  ensureProxyDir() {
188642
- if (!fs18.existsSync(this.proxyDir)) {
188643
- fs18.mkdirSync(this.proxyDir, { recursive: true });
188859
+ if (!fs19.existsSync(this.proxyDir)) {
188860
+ fs19.mkdirSync(this.proxyDir, { recursive: true });
188644
188861
  }
188645
188862
  }
188646
188863
  isProcessRunning(pid) {
@@ -188697,15 +188914,15 @@ var ProxyRegistryManager = class {
188697
188914
  const startTime = Date.now();
188698
188915
  while (Date.now() - startTime < timeoutMs) {
188699
188916
  try {
188700
- const fd = fs18.openSync(
188917
+ const fd = fs19.openSync(
188701
188918
  this.lockPath,
188702
- fs18.constants.O_CREAT | fs18.constants.O_EXCL | fs18.constants.O_WRONLY
188919
+ fs19.constants.O_CREAT | fs19.constants.O_EXCL | fs19.constants.O_WRONLY
188703
188920
  );
188704
- fs18.writeSync(fd, String(process.pid));
188705
- fs18.closeSync(fd);
188921
+ fs19.writeSync(fd, String(process.pid));
188922
+ fs19.closeSync(fd);
188706
188923
  return () => {
188707
188924
  try {
188708
- fs18.unlinkSync(this.lockPath);
188925
+ fs19.unlinkSync(this.lockPath);
188709
188926
  } catch {
188710
188927
  }
188711
188928
  };
@@ -188714,16 +188931,16 @@ var ProxyRegistryManager = class {
188714
188931
  if (err.code === "EEXIST") {
188715
188932
  try {
188716
188933
  const lockPid = parseInt(
188717
- fs18.readFileSync(this.lockPath, "utf-8").trim(),
188934
+ fs19.readFileSync(this.lockPath, "utf-8").trim(),
188718
188935
  10
188719
188936
  );
188720
188937
  if (!this.isProcessRunning(lockPid)) {
188721
- fs18.unlinkSync(this.lockPath);
188938
+ fs19.unlinkSync(this.lockPath);
188722
188939
  continue;
188723
188940
  }
188724
188941
  } catch {
188725
188942
  try {
188726
- fs18.unlinkSync(this.lockPath);
188943
+ fs19.unlinkSync(this.lockPath);
188727
188944
  } catch {
188728
188945
  }
188729
188946
  continue;
@@ -188743,8 +188960,8 @@ var ProxyRegistryManager = class {
188743
188960
  async claimProxyOwnership(key) {
188744
188961
  const releaseLock = await this.acquireLock();
188745
188962
  try {
188746
- if (fs18.existsSync(this.ownerPath)) {
188747
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188963
+ if (fs19.existsSync(this.ownerPath)) {
188964
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188748
188965
  const ownerFile2 = JSON.parse(content);
188749
188966
  if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188750
188967
  return false;
@@ -188774,11 +188991,11 @@ var ProxyRegistryManager = class {
188774
188991
  }
188775
188992
  const releaseLock = await this.acquireLock();
188776
188993
  try {
188777
- if (fs18.existsSync(this.ownerPath)) {
188778
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
188994
+ if (fs19.existsSync(this.ownerPath)) {
188995
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188779
188996
  const ownerFile = JSON.parse(content);
188780
188997
  if (ownerFile.owner.pid === process.pid) {
188781
- fs18.unlinkSync(this.ownerPath);
188998
+ fs19.unlinkSync(this.ownerPath);
188782
188999
  }
188783
189000
  }
188784
189001
  this.isOwner = false;
@@ -188790,12 +189007,12 @@ var ProxyRegistryManager = class {
188790
189007
  * Get the current proxy owner.
188791
189008
  */
188792
189009
  async getProxyOwner() {
188793
- if (!fs18.existsSync(this.ownerPath)) {
189010
+ if (!fs19.existsSync(this.ownerPath)) {
188794
189011
  return null;
188795
189012
  }
188796
189013
  const releaseLock = await this.acquireLock();
188797
189014
  try {
188798
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
189015
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188799
189016
  const ownerFile = JSON.parse(content);
188800
189017
  if (!await this.isProxyOwnerHealthy(ownerFile.owner.pid)) {
188801
189018
  return null;
@@ -188889,7 +189106,7 @@ var ProxyRegistryManager = class {
188889
189106
  */
188890
189107
  watchRegistry(onChange) {
188891
189108
  this.ensureProxyDir();
188892
- if (!fs18.existsSync(this.registryPath)) {
189109
+ if (!fs19.existsSync(this.registryPath)) {
188893
189110
  const emptyRegistry = {
188894
189111
  version: 1,
188895
189112
  keys: {},
@@ -188933,13 +189150,13 @@ var ProxyRegistryManager = class {
188933
189150
  async attemptElection(key) {
188934
189151
  const releaseLock = await this.acquireLock();
188935
189152
  try {
188936
- if (fs18.existsSync(this.ownerPath)) {
188937
- const content = fs18.readFileSync(this.ownerPath, "utf-8");
189153
+ if (fs19.existsSync(this.ownerPath)) {
189154
+ const content = fs19.readFileSync(this.ownerPath, "utf-8");
188938
189155
  const ownerFile2 = JSON.parse(content);
188939
189156
  if (await this.isProxyOwnerHealthy(ownerFile2.owner.pid)) {
188940
189157
  return false;
188941
189158
  }
188942
- fs18.unlinkSync(this.ownerPath);
189159
+ fs19.unlinkSync(this.ownerPath);
188943
189160
  }
188944
189161
  const ownerFile = {
188945
189162
  version: 1,
@@ -188957,7 +189174,7 @@ var ProxyRegistryManager = class {
188957
189174
  }
188958
189175
  }
188959
189176
  readRegistry() {
188960
- if (!fs18.existsSync(this.registryPath)) {
189177
+ if (!fs19.existsSync(this.registryPath)) {
188961
189178
  return {
188962
189179
  version: 1,
188963
189180
  keys: {},
@@ -188965,7 +189182,7 @@ var ProxyRegistryManager = class {
188965
189182
  };
188966
189183
  }
188967
189184
  try {
188968
- const content = fs18.readFileSync(this.registryPath, "utf-8");
189185
+ const content = fs19.readFileSync(this.registryPath, "utf-8");
188969
189186
  return JSON.parse(content);
188970
189187
  } catch {
188971
189188
  return {
@@ -188978,8 +189195,8 @@ var ProxyRegistryManager = class {
188978
189195
  writeFileAtomic(filePath, data) {
188979
189196
  this.ensureProxyDir();
188980
189197
  const tmpPath = filePath + ".tmp";
188981
- fs18.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
188982
- fs18.renameSync(tmpPath, filePath);
189198
+ fs19.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
189199
+ fs19.renameSync(tmpPath, filePath);
188983
189200
  }
188984
189201
  };
188985
189202
 
@@ -189078,17 +189295,12 @@ function ConfigInput({ configName, onSubmit, onCancel }) {
189078
189295
  return /* @__PURE__ */ React5.createElement(Box5, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React5.createElement(Text5, null, "Enter value for config ", /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, configName), ":"), /* @__PURE__ */ React5.createElement(Box5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan" }, "> "), /* @__PURE__ */ React5.createElement(Text5, null, value), /* @__PURE__ */ React5.createElement(Text5, { color: "gray" }, "|")), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(Press Enter to save, Esc to cancel)"));
189079
189296
  }
189080
189297
 
189081
- // src/lib/ui/interactive.ts
189082
- function isInteractive() {
189083
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
189084
- }
189085
-
189086
189298
  // src/commands/dev.tsx
189087
189299
  var COLORS = ["cyan", "yellow", "green", "magenta", "blue"];
189088
- function DevUI({ instanceKey }) {
189300
+ function DevUI({ instanceKey, tunnelEnabled }) {
189089
189301
  const { exit } = useApp2();
189090
189302
  const [state, setState] = useState5(() => {
189091
- const caExists = caFilesExist();
189303
+ const caExists = tunnelEnabled || caFilesExist();
189092
189304
  return {
189093
189305
  status: caExists ? "loading" : "installing-ca",
189094
189306
  ...caExists ? {} : { caInstallPhase: "installing" },
@@ -189097,7 +189309,9 @@ function DevUI({ instanceKey }) {
189097
189309
  services: [],
189098
189310
  output: [],
189099
189311
  colorMap: /* @__PURE__ */ new Map(),
189100
- isProxyOwner: false
189312
+ isProxyOwner: false,
189313
+ tunnels: /* @__PURE__ */ new Map(),
189314
+ tunnelStatus: /* @__PURE__ */ new Map()
189101
189315
  };
189102
189316
  });
189103
189317
  useEffect3(() => {
@@ -189129,6 +189343,7 @@ function DevUI({ instanceKey }) {
189129
189343
  const drizzleGatewayRef = useRef(null);
189130
189344
  const registryWatcherCleanupRef = useRef(null);
189131
189345
  const electionIntervalRef = useRef(null);
189346
+ const tunnelsRef = useRef([]);
189132
189347
  const proxyRef = useRef(null);
189133
189348
  const adminServerRef = useRef(null);
189134
189349
  const servicesRef = useRef([]);
@@ -189162,7 +189377,9 @@ function DevUI({ instanceKey }) {
189162
189377
  // Stop Drizzle Gateway
189163
189378
  drizzleGatewayRef.current?.stop(),
189164
189379
  // Stop all resources
189165
- ...[...resourcesRef.current.values()].map((resource) => resource.stop())
189380
+ ...[...resourcesRef.current.values()].map((resource) => resource.stop()),
189381
+ // Stop all tunnels
189382
+ ...tunnelsRef.current.map((tunnel) => tunnel.stop())
189166
189383
  ]);
189167
189384
  if (proxyRegistryRef.current) {
189168
189385
  await proxyRegistryRef.current.unregisterServices(instanceKey);
@@ -189199,14 +189416,20 @@ function DevUI({ instanceKey }) {
189199
189416
  // Stop Drizzle Gateway
189200
189417
  drizzleGatewayRef.current?.stop(),
189201
189418
  // Stop all resources
189202
- ...[...resourcesRef.current.values()].map((resource) => resource.stop())
189419
+ ...[...resourcesRef.current.values()].map((resource) => resource.stop()),
189420
+ // Stop all tunnels
189421
+ ...tunnelsRef.current.map((tunnel) => tunnel.stop())
189203
189422
  ]);
189423
+ if (tunnelsRef.current.length > 0) {
189424
+ await new Promise((resolve7) => setTimeout(resolve7, 1500));
189425
+ }
189204
189426
  electricInstancesRef.current = [];
189205
189427
  drizzleGatewayRef.current = null;
189206
189428
  proxyRef.current = null;
189207
189429
  adminServerRef.current = null;
189208
189430
  servicesRef.current = [];
189209
189431
  resourcesRef.current = /* @__PURE__ */ new Map();
189432
+ tunnelsRef.current = [];
189210
189433
  if (proxyRegistryRef.current) {
189211
189434
  await proxyRegistryRef.current.unregisterServices(instanceKey);
189212
189435
  await proxyRegistryRef.current.releaseProxyOwnership();
@@ -189222,7 +189445,9 @@ function DevUI({ instanceKey }) {
189222
189445
  resourceStatus: /* @__PURE__ */ new Map(),
189223
189446
  services: [],
189224
189447
  proxy: void 0,
189225
- isProxyOwner: false
189448
+ isProxyOwner: false,
189449
+ tunnels: /* @__PURE__ */ new Map(),
189450
+ tunnelStatus: /* @__PURE__ */ new Map()
189226
189451
  }));
189227
189452
  setReloadTrigger((t) => t + 1);
189228
189453
  };
@@ -189263,10 +189488,10 @@ function DevUI({ instanceKey }) {
189263
189488
  }, [state.status]);
189264
189489
  useEffect3(() => {
189265
189490
  if (state.status !== "running") return;
189266
- const configPath = path16.join(process.cwd(), "specific.hcl");
189491
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189267
189492
  const watcher = watchConfigFile(configPath, 1e3, () => {
189268
189493
  try {
189269
- const hcl = fs19.readFileSync(configPath, "utf-8");
189494
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189270
189495
  parseConfig(hcl).then(() => {
189271
189496
  triggerReload();
189272
189497
  }).catch((err) => {
@@ -189391,8 +189616,8 @@ function DevUI({ instanceKey }) {
189391
189616
  }));
189392
189617
  return;
189393
189618
  }
189394
- const configPath = path16.join(process.cwd(), "specific.hcl");
189395
- if (!fs19.existsSync(configPath)) {
189619
+ const configPath = path17.join(process.cwd(), "specific.hcl");
189620
+ if (!fs20.existsSync(configPath)) {
189396
189621
  writeLog("system", "Waiting for specific.hcl to appear");
189397
189622
  setState((s) => ({
189398
189623
  ...s,
@@ -189411,7 +189636,7 @@ function DevUI({ instanceKey }) {
189411
189636
  }
189412
189637
  let config2;
189413
189638
  try {
189414
- const hcl = fs19.readFileSync(configPath, "utf-8");
189639
+ const hcl = fs20.readFileSync(configPath, "utf-8");
189415
189640
  config2 = await parseConfig(hcl);
189416
189641
  } catch (err) {
189417
189642
  setState((s) => ({
@@ -189517,7 +189742,7 @@ function DevUI({ instanceKey }) {
189517
189742
  const drizzleGateway = await startDrizzleGateway(
189518
189743
  postgresResources,
189519
189744
  drizzlePort,
189520
- path16.join(process.cwd(), ".specific", "keys", instanceKey)
189745
+ path17.join(process.cwd(), ".specific", "keys", instanceKey)
189521
189746
  );
189522
189747
  startedDrizzleGateway = drizzleGateway;
189523
189748
  drizzleGatewayRef.current = drizzleGateway;
@@ -189617,6 +189842,39 @@ Add them to the config block in specific.local`);
189617
189842
  serviceEndpointPorts.set(service.name, endpointPorts);
189618
189843
  serviceEndpoints.set(service.name, endpointInfos);
189619
189844
  }
189845
+ const serviceInfos = [];
189846
+ const exposedServices = [];
189847
+ for (const service of config2.services) {
189848
+ const endpointInfos = serviceEndpoints.get(service.name) || [];
189849
+ for (const info of endpointInfos) {
189850
+ const endpointConfig = service.endpoints.find(
189851
+ (e) => e.name === info.endpointName
189852
+ );
189853
+ if (endpointConfig?.public) {
189854
+ const proxyName = info.endpointName === "default" ? service.name : `${service.name}-${info.endpointName}`;
189855
+ exposedServices.push({ name: proxyName, port: info.port });
189856
+ serviceInfos.push({
189857
+ serviceName: proxyName,
189858
+ port: info.port
189859
+ });
189860
+ }
189861
+ }
189862
+ }
189863
+ const publicUrls = /* @__PURE__ */ new Map();
189864
+ let subdomainAllocator;
189865
+ if (tunnelEnabled && exposedServices.length > 0) {
189866
+ subdomainAllocator = new StableSubdomainAllocator(process.cwd(), instanceKey);
189867
+ const multipleServices = exposedServices.length > 1;
189868
+ for (const svc of exposedServices) {
189869
+ const subdomain = subdomainAllocator.allocate(svc.name, multipleServices);
189870
+ publicUrls.set(svc.name, `${subdomain}.tunnel.spcf.app`);
189871
+ }
189872
+ } else {
189873
+ const keySuffix = instanceKey === "default" ? "" : `.${instanceKey}`;
189874
+ for (const svc of exposedServices) {
189875
+ publicUrls.set(svc.name, `${svc.name}${keySuffix}.local.spcf.app`);
189876
+ }
189877
+ }
189620
189878
  const services2 = [];
189621
189879
  for (const service of config2.services) {
189622
189880
  if (cancelled) break;
@@ -189640,7 +189898,8 @@ Add them to the config block in specific.local`);
189640
189898
  configs,
189641
189899
  endpointPorts,
189642
189900
  serviceEndpoints,
189643
- (line) => addLog(line, colorMap)
189901
+ (line) => addLog(line, colorMap),
189902
+ publicUrls
189644
189903
  );
189645
189904
  services2.push(running);
189646
189905
  startedServices.push(running);
@@ -189668,154 +189927,195 @@ Add them to the config block in specific.local`);
189668
189927
  }
189669
189928
  }
189670
189929
  if (cancelled) return;
189671
- const serviceInfos = [];
189672
- const exposedServices = [];
189673
- for (const service of config2.services) {
189674
- const endpointInfos = serviceEndpoints.get(service.name) || [];
189675
- for (const info of endpointInfos) {
189676
- const endpointConfig = service.endpoints.find(
189677
- (e) => e.name === info.endpointName
189678
- );
189679
- if (endpointConfig?.public) {
189680
- const proxyName = info.endpointName === "default" ? service.name : `${service.name}-${info.endpointName}`;
189681
- exposedServices.push({ name: proxyName, port: info.port });
189682
- serviceInfos.push({
189683
- serviceName: proxyName,
189684
- port: info.port
189685
- });
189686
- }
189687
- }
189688
- }
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);
189930
+ if (tunnelEnabled) {
189931
+ writeLog("system", "Tunnel mode enabled, starting tunnels for public services");
189932
+ if (exposedServices.length === 0) {
189933
+ writeLog("system", "No public services to tunnel");
189934
+ } else {
189935
+ const multipleServices = exposedServices.length > 1;
189936
+ const tunnelInstances = [];
189937
+ const tunnelStatusMap = /* @__PURE__ */ new Map();
189938
+ for (const svc of exposedServices) {
189939
+ tunnelStatusMap.set(svc.name, "connecting");
189940
+ }
189941
+ setState((s) => ({ ...s, tunnelStatus: new Map(tunnelStatusMap) }));
189942
+ for (const svc of exposedServices) {
189943
+ if (cancelled) return;
189944
+ const subdomain = subdomainAllocator.allocate(svc.name, multipleServices);
189945
+ writeLog("system", `Starting tunnel for ${svc.name} on port ${svc.port} (subdomain: ${subdomain})`);
189946
+ try {
189947
+ const tunnel = await startTunnel(
189948
+ svc.name,
189949
+ "default",
189950
+ svc.port,
189951
+ subdomain,
189952
+ {
189953
+ onError: (serviceName, _endpointName, error) => {
189954
+ writeLog("system:error", `Tunnel error for ${serviceName}: ${error.message}`);
189955
+ },
189956
+ onClose: (serviceName) => {
189957
+ writeLog("system", `Tunnel closed for ${serviceName}`);
189958
+ }
189959
+ }
189960
+ );
189961
+ tunnelInstances.push(tunnel);
189962
+ tunnelsRef.current = [...tunnelInstances];
189963
+ tunnelStatusMap.set(svc.name, "connected");
189964
+ setState((s) => ({
189965
+ ...s,
189966
+ tunnels: new Map([...s.tunnels, [svc.name, tunnel]]),
189967
+ tunnelStatus: new Map(tunnelStatusMap)
189968
+ }));
189969
+ writeLog("system", `Tunnel ready for ${svc.name}: ${tunnel.url}`);
189970
+ } catch (err) {
189971
+ const errorMsg = `Failed to start tunnel for ${svc.name}: ${err instanceof Error ? err.message : String(err)}`;
189972
+ writeLog("system:error", errorMsg);
189973
+ tunnelStatusMap.set(svc.name, "error");
189974
+ for (const t of tunnelInstances) {
189975
+ await t.stop();
189746
189976
  }
189747
- const allKeys = [...knownKeys];
189748
- const newCertificate = generateCertificate("local.spcf.app", allKeys);
189749
- proxy2.updateCertificate(newCertificate);
189977
+ setState((s) => ({
189978
+ ...s,
189979
+ status: "error",
189980
+ error: errorMsg,
189981
+ tunnelStatus: new Map(tunnelStatusMap)
189982
+ }));
189983
+ return;
189750
189984
  }
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;
189985
+ }
189764
189986
  }
189765
189987
  } else {
189766
- writeLog("system", "Another instance owns the proxy, starting election watcher");
189767
- setState((s) => ({ ...s, isProxyOwner: false }));
189768
- const isProcessRunning = (pid) => {
189988
+ const runningServicePorts = /* @__PURE__ */ new Map();
189989
+ for (const s of services2) {
189990
+ runningServicePorts.set(s.name, s.ports.get("default"));
189991
+ }
189992
+ const projectId = hasProjectId() ? readProjectId() : void 0;
189993
+ const getState = () => ({
189994
+ status: "running",
189995
+ services: config2.services.filter((svc) => runningServicePorts.has(svc.name) || svc.serve).map((svc) => ({
189996
+ name: svc.name,
189997
+ port: runningServicePorts.get(svc.name),
189998
+ exposed: !!svc.serve || svc.endpoints.some((e) => e.public),
189999
+ env: svc.env
190000
+ })),
190001
+ resources: [...resources2.entries()].map(([name, r]) => ({
190002
+ name,
190003
+ type: r.type,
190004
+ port: r.port,
190005
+ host: r.host,
190006
+ syncEnabled: r.type === "postgres" && syncDatabases.has(name)
190007
+ })),
190008
+ projectId
190009
+ });
190010
+ const adminServer = await startAdminServer(getState);
190011
+ adminServerRef.current = adminServer;
190012
+ writeLog("system", `Admin API server started on port ${adminServer.port}`);
190013
+ await proxyRegistry.registerServices(
190014
+ instanceKey,
190015
+ adminServer.port,
190016
+ serviceInfos,
190017
+ startedDrizzleGateway?.port
190018
+ );
190019
+ writeLog("system", `Registered ${serviceInfos.length} services with proxy registry`);
190020
+ const becameProxyOwner = await proxyRegistry.claimProxyOwnership(instanceKey);
190021
+ if (becameProxyOwner) {
190022
+ writeLog("system", "Claimed proxy ownership, starting HTTP proxy");
189769
190023
  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
- }
190024
+ const currentServices = await proxyRegistry.getAllServices();
190025
+ const registeredKeys = [...new Set(currentServices.map((s) => s.key))];
190026
+ const certificate = generateCertificate("local.spcf.app", registeredKeys);
190027
+ const proxy2 = await startHttpProxy(
190028
+ exposedServices,
190029
+ certificate,
190030
+ getState,
190031
+ instanceKey
190032
+ );
190033
+ startedProxy = proxy2;
190034
+ proxyRef.current = proxy2;
190035
+ setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
190036
+ const knownKeys = new Set(registeredKeys);
190037
+ registryWatcherCleanupRef.current = proxyRegistry.watchRegistry(async (updatedServices, updatedKeys) => {
190038
+ writeLog("system", `Registry updated: ${updatedServices.length} services`);
190039
+ proxy2.updateServices(updatedServices, updatedKeys);
190040
+ const newKeyNames = Object.keys(updatedKeys).filter((k) => !knownKeys.has(k));
190041
+ if (newKeyNames.length > 0) {
190042
+ writeLog("system", `New keys detected: ${newKeyNames.join(", ")} - regenerating certificate`);
190043
+ for (const key of newKeyNames) {
190044
+ knownKeys.add(key);
190045
+ }
190046
+ const allKeys = [...knownKeys];
190047
+ const newCertificate = generateCertificate("local.spcf.app", allKeys);
190048
+ proxy2.updateCertificate(newCertificate);
190049
+ }
190050
+ });
190051
+ const currentKeys = await proxyRegistry.getAllKeyRegistrations();
190052
+ proxy2.updateServices(currentServices, currentKeys);
190053
+ writeLog("system", `Loaded ${currentServices.length} services from registry`);
190054
+ } catch (err) {
190055
+ const errorMsg = `Failed to start HTTP proxy: ${err instanceof Error ? err.message : String(err)}`;
190056
+ writeLog("system:error", errorMsg);
190057
+ setState((s) => ({
190058
+ ...s,
190059
+ status: "error",
190060
+ error: errorMsg
190061
+ }));
189782
190062
  return;
189783
190063
  }
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");
190064
+ } else {
190065
+ writeLog("system", "Another instance owns the proxy, starting election watcher");
190066
+ setState((s) => ({ ...s, isProxyOwner: false }));
190067
+ const isProcessRunning = (pid) => {
190068
+ try {
190069
+ process.kill(pid, 0);
190070
+ return true;
190071
+ } catch {
190072
+ return false;
190073
+ }
190074
+ };
190075
+ electionIntervalRef.current = setInterval(async () => {
190076
+ if (cancelled || shuttingDown.current) {
189790
190077
  if (electionIntervalRef.current) {
189791
190078
  clearInterval(electionIntervalRef.current);
189792
190079
  electionIntervalRef.current = null;
189793
190080
  }
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}`);
190081
+ return;
190082
+ }
190083
+ const owner = await proxyRegistry.getProxyOwner();
190084
+ if (!owner || !isProcessRunning(owner.pid)) {
190085
+ writeLog("system", "Proxy owner died, attempting election");
190086
+ const won = await proxyRegistry.attemptElection(instanceKey);
190087
+ if (won) {
190088
+ writeLog("system", "Won election, starting HTTP proxy");
190089
+ if (electionIntervalRef.current) {
190090
+ clearInterval(electionIntervalRef.current);
190091
+ electionIntervalRef.current = null;
190092
+ }
190093
+ try {
190094
+ const electionServices = await proxyRegistry.getAllServices();
190095
+ const electionKeyRegistrations = await proxyRegistry.getAllKeyRegistrations();
190096
+ const electionKeyNames = Object.keys(electionKeyRegistrations);
190097
+ const certificate = generateCertificate("local.spcf.app", electionKeyNames);
190098
+ const proxy2 = await startHttpProxy(
190099
+ exposedServices,
190100
+ certificate,
190101
+ getState,
190102
+ instanceKey
190103
+ );
190104
+ startedProxy = proxy2;
190105
+ proxyRef.current = proxy2;
190106
+ setState((s) => ({ ...s, proxy: proxy2, isProxyOwner: true }));
190107
+ registryWatcherCleanupRef.current = proxyRegistry.watchRegistry((updatedServices, updatedKeys) => {
190108
+ writeLog("system", `Registry updated: ${updatedServices.length} services`);
190109
+ proxy2.updateServices(updatedServices, updatedKeys);
190110
+ });
190111
+ proxy2.updateServices(electionServices, electionKeyRegistrations);
190112
+ } catch (err) {
190113
+ writeLog("system:error", `Failed to start proxy after election: ${err}`);
190114
+ }
189815
190115
  }
189816
190116
  }
189817
- }
189818
- }, 1e3);
190117
+ }, 1e3);
190118
+ }
189819
190119
  }
189820
190120
  if (cancelled) return;
189821
190121
  writeLog("system", "Dev server running");
@@ -189961,39 +190261,58 @@ Add them to the config block in specific.local`);
189961
190261
  const staticItems = [
189962
190262
  {
189963
190263
  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)"))
190264
+ 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
190265
  },
189966
190266
  { 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}`;
190267
+ // Show admin UI URL (only in non-tunnel mode)
190268
+ ...!tunnelEnabled ? [
190269
+ {
190270
+ key: "admin",
190271
+ 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"))
190272
+ },
190273
+ { key: "admin-space", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190274
+ ] : [],
190275
+ // Services section - different rendering for tunnel mode vs local mode
190276
+ ...tunnelEnabled ? (
190277
+ // Tunnel mode: show tunnel URLs for public services
190278
+ state.tunnels.size > 0 || state.tunnelStatus.size > 0 ? [
190279
+ { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
190280
+ ...[...state.tunnelStatus.entries()].map(([serviceName, status]) => {
190281
+ const tunnel = state.tunnels.get(serviceName);
189989
190282
  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)
190283
+ key: `svc-${serviceName}`,
190284
+ 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
190285
  };
189993
- });
189994
- }),
189995
- { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
189996
- ] : [],
190286
+ }),
190287
+ { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190288
+ ] : []
190289
+ ) : (
190290
+ // Local mode: show local URLs
190291
+ services.length > 0 ? [
190292
+ { key: "svc-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Services:") },
190293
+ ...services.flatMap((svc) => {
190294
+ const serviceConfig = config.services.find((s) => s.name === svc.name);
190295
+ const endpoints = serviceConfig?.endpoints || [];
190296
+ if (endpoints.length === 0 && svc.ports.size > 0) {
190297
+ const defaultPort = svc.ports.get("default");
190298
+ return [{
190299
+ key: `svc-${svc.name}`,
190300
+ 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, ")"))
190301
+ }];
190302
+ }
190303
+ return endpoints.map((endpoint) => {
190304
+ const port = svc.ports.get(endpoint.name);
190305
+ const displayName = endpoint.name === "default" ? svc.name : `${svc.name}:${endpoint.name}`;
190306
+ const proxyName = endpoint.name === "default" ? svc.name : `${svc.name}-${endpoint.name}`;
190307
+ return {
190308
+ key: `svc-${svc.name}-${endpoint.name}`,
190309
+ 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)
190310
+ };
190311
+ });
190312
+ }),
190313
+ { key: "space2", content: /* @__PURE__ */ React6.createElement(Text6, null, " ") }
190314
+ ] : []
190315
+ ),
189997
190316
  ...config.postgres.length > 0 ? [
189998
190317
  { key: "pg-header", content: /* @__PURE__ */ React6.createElement(Text6, { bold: true }, "Postgres:") },
189999
190318
  ...config.postgres.map((pg) => {
@@ -190041,13 +190360,13 @@ Add them to the config block in specific.local`);
190041
190360
  ];
190042
190361
  return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Static, { items: staticItems }, (item) => /* @__PURE__ */ React6.createElement(Box6, { key: item.key }, item.content)));
190043
190362
  }
190044
- function devCommand(instanceKey) {
190045
- render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey }));
190363
+ function devCommand(instanceKey, tunnelEnabled = false) {
190364
+ render4(/* @__PURE__ */ React6.createElement(DevUI, { instanceKey, tunnelEnabled }));
190046
190365
  }
190047
190366
 
190048
190367
  // src/lib/dev/git-worktree.ts
190049
190368
  import { execSync as execSync2 } from "child_process";
190050
- import * as path17 from "path";
190369
+ import * as path18 from "path";
190051
190370
  function isInWorktree() {
190052
190371
  try {
190053
190372
  const commonDir = execSync2("git rev-parse --git-common-dir", {
@@ -190058,8 +190377,8 @@ function isInWorktree() {
190058
190377
  encoding: "utf-8",
190059
190378
  stdio: ["pipe", "pipe", "pipe"]
190060
190379
  }).trim();
190061
- const resolvedCommonDir = path17.resolve(commonDir);
190062
- const resolvedGitDir = path17.resolve(gitDir);
190380
+ const resolvedCommonDir = path18.resolve(commonDir);
190381
+ const resolvedGitDir = path18.resolve(gitDir);
190063
190382
  return resolvedCommonDir !== resolvedGitDir;
190064
190383
  } catch {
190065
190384
  return false;
@@ -190074,7 +190393,7 @@ function getWorktreeName() {
190074
190393
  encoding: "utf-8",
190075
190394
  stdio: ["pipe", "pipe", "pipe"]
190076
190395
  }).trim();
190077
- return path17.basename(gitDir);
190396
+ return path18.basename(gitDir);
190078
190397
  } catch {
190079
190398
  return null;
190080
190399
  }
@@ -190089,35 +190408,35 @@ init_open();
190089
190408
  import React7, { useState as useState6, useEffect as useEffect4, useCallback } from "react";
190090
190409
  import { render as render5, Text as Text7, Box as Box7, useApp as useApp3, useInput as useInput5 } from "ink";
190091
190410
  import Spinner5 from "ink-spinner";
190092
- import * as fs21 from "fs";
190093
- import * as path19 from "path";
190411
+ import * as fs22 from "fs";
190412
+ import * as path20 from "path";
190094
190413
 
190095
190414
  // src/lib/deploy/build-tester.ts
190096
190415
  import { spawn as spawn5 } from "child_process";
190097
- import { existsSync as existsSync17 } from "fs";
190098
- import { join as join18 } from "path";
190416
+ import { existsSync as existsSync18 } from "fs";
190417
+ import { join as join19 } from "path";
190099
190418
  function getDependencyInstallCommand(build, projectDir) {
190100
190419
  switch (build.base) {
190101
190420
  case "node":
190102
- if (existsSync17(join18(projectDir, "pnpm-lock.yaml"))) {
190421
+ if (existsSync18(join19(projectDir, "pnpm-lock.yaml"))) {
190103
190422
  return "pnpm install --frozen-lockfile";
190104
- } else if (existsSync17(join18(projectDir, "yarn.lock"))) {
190423
+ } else if (existsSync18(join19(projectDir, "yarn.lock"))) {
190105
190424
  return "yarn install --frozen-lockfile";
190106
- } else if (existsSync17(join18(projectDir, "package-lock.json"))) {
190425
+ } else if (existsSync18(join19(projectDir, "package-lock.json"))) {
190107
190426
  return "npm ci";
190108
190427
  } else {
190109
190428
  return "npm install";
190110
190429
  }
190111
190430
  case "python":
190112
- if (existsSync17(join18(projectDir, "poetry.lock"))) {
190431
+ if (existsSync18(join19(projectDir, "poetry.lock"))) {
190113
190432
  return "poetry install --no-interaction";
190114
- } else if (existsSync17(join18(projectDir, "Pipfile.lock"))) {
190433
+ } else if (existsSync18(join19(projectDir, "Pipfile.lock"))) {
190115
190434
  return "pipenv install --deploy";
190116
- } else if (existsSync17(join18(projectDir, "Pipfile"))) {
190435
+ } else if (existsSync18(join19(projectDir, "Pipfile"))) {
190117
190436
  return "pipenv install";
190118
- } else if (existsSync17(join18(projectDir, "pyproject.toml"))) {
190437
+ } else if (existsSync18(join19(projectDir, "pyproject.toml"))) {
190119
190438
  return "pip install .";
190120
- } else if (existsSync17(join18(projectDir, "requirements.txt"))) {
190439
+ } else if (existsSync18(join19(projectDir, "requirements.txt"))) {
190121
190440
  return "pip install -r requirements.txt";
190122
190441
  }
190123
190442
  return null;
@@ -190263,8 +190582,8 @@ async function testAllBuilds(builds, projectDir) {
190263
190582
 
190264
190583
  // src/lib/tarball/create.ts
190265
190584
  import { execSync as execSync3 } from "child_process";
190266
- import * as fs20 from "fs";
190267
- import * as path18 from "path";
190585
+ import * as fs21 from "fs";
190586
+ import * as path19 from "path";
190268
190587
  import { createTarPacker, createEntryItemGenerator } from "tar-vern";
190269
190588
  function isInsideGitRepository(dir) {
190270
190589
  try {
@@ -190321,10 +190640,10 @@ var EXCLUDED_DIRS = [
190321
190640
  ];
190322
190641
  async function collectPaths(baseDir, currentDir, exclude) {
190323
190642
  const results = [];
190324
- const entries = await fs20.promises.readdir(currentDir, { withFileTypes: true });
190643
+ const entries = await fs21.promises.readdir(currentDir, { withFileTypes: true });
190325
190644
  for (const entry of entries) {
190326
- const fullPath = path18.join(currentDir, entry.name);
190327
- const relativePath = path18.relative(baseDir, fullPath);
190645
+ const fullPath = path19.join(currentDir, entry.name);
190646
+ const relativePath = path19.relative(baseDir, fullPath);
190328
190647
  if (entry.isDirectory()) {
190329
190648
  if (!exclude.includes(entry.name)) {
190330
190649
  results.push(relativePath);
@@ -190339,8 +190658,8 @@ async function collectPaths(baseDir, currentDir, exclude) {
190339
190658
  }
190340
190659
  async function createTarArchive(projectDir) {
190341
190660
  writeLog("tarball", "Creating tarball using tar-vern (non-git project)");
190342
- const configPath = path18.join(projectDir, "specific.hcl");
190343
- if (!fs20.existsSync(configPath)) {
190661
+ const configPath = path19.join(projectDir, "specific.hcl");
190662
+ if (!fs21.existsSync(configPath)) {
190344
190663
  throw new Error("specific.hcl not found in project directory");
190345
190664
  }
190346
190665
  const relativePaths = await collectPaths(projectDir, projectDir, EXCLUDED_DIRS);
@@ -190357,8 +190676,8 @@ async function createTarArchive(projectDir) {
190357
190676
  }
190358
190677
  function findWidestContext(projectDir, contexts) {
190359
190678
  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));
190679
+ const absolute = contexts.map((c) => path19.resolve(projectDir, c));
190680
+ const segments = absolute.map((p) => p.split(path19.sep).filter(Boolean));
190362
190681
  const firstSegments = segments[0];
190363
190682
  if (!firstSegments) return ".";
190364
190683
  const minLen = Math.min(...segments.map((s) => s.length));
@@ -190372,12 +190691,12 @@ function findWidestContext(projectDir, contexts) {
190372
190691
  }
190373
190692
  }
190374
190693
  const ancestorSegments = firstSegments.slice(0, commonLength);
190375
- const ancestor = path18.sep + ancestorSegments.join(path18.sep);
190376
- return path18.relative(projectDir, ancestor) || ".";
190694
+ const ancestor = path19.sep + ancestorSegments.join(path19.sep);
190695
+ return path19.relative(projectDir, ancestor) || ".";
190377
190696
  }
190378
190697
  async function createProjectTarball(projectDir, context = ".") {
190379
- const contextDir = path18.resolve(projectDir, context);
190380
- const appPath = path18.relative(contextDir, projectDir) || ".";
190698
+ const contextDir = path19.resolve(projectDir, context);
190699
+ const appPath = path19.relative(contextDir, projectDir) || ".";
190381
190700
  writeLog("tarball", `Context: ${contextDir}, appPath: ${appPath}`);
190382
190701
  let tarball;
190383
190702
  if (isInsideGitRepository(contextDir)) {
@@ -191264,14 +191583,14 @@ ${errorMsg}`
191264
191583
  ), 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
191584
  }
191266
191585
  async function deployCommand(environment, options2) {
191267
- const configPath = path19.join(process.cwd(), "specific.hcl");
191268
- if (!fs21.existsSync(configPath)) {
191586
+ const configPath = path20.join(process.cwd(), "specific.hcl");
191587
+ if (!fs22.existsSync(configPath)) {
191269
191588
  console.error("Error: No specific.hcl found in current directory");
191270
191589
  process.exit(1);
191271
191590
  }
191272
191591
  let config;
191273
191592
  try {
191274
- const hcl = fs21.readFileSync(configPath, "utf-8");
191593
+ const hcl = fs22.readFileSync(configPath, "utf-8");
191275
191594
  config = await parseConfig(hcl);
191276
191595
  } catch (err) {
191277
191596
  console.error(
@@ -191295,8 +191614,8 @@ async function deployCommand(environment, options2) {
191295
191614
 
191296
191615
  // src/commands/exec.tsx
191297
191616
  import { spawn as spawn6 } from "child_process";
191298
- import * as fs22 from "fs";
191299
- import * as path20 from "path";
191617
+ import * as fs23 from "fs";
191618
+ import * as path21 from "path";
191300
191619
  async function execCommand(serviceName, command, instanceKey = "default") {
191301
191620
  if (command.length === 0) {
191302
191621
  console.error(
@@ -191324,14 +191643,14 @@ async function execCommand(serviceName, command, instanceKey = "default") {
191324
191643
  }
191325
191644
  }
191326
191645
  };
191327
- const configPath = path20.join(process.cwd(), "specific.hcl");
191328
- if (!fs22.existsSync(configPath)) {
191646
+ const configPath = path21.join(process.cwd(), "specific.hcl");
191647
+ if (!fs23.existsSync(configPath)) {
191329
191648
  console.error("Error: No specific.hcl found in current directory");
191330
191649
  process.exit(1);
191331
191650
  }
191332
191651
  let config;
191333
191652
  try {
191334
- const hcl = fs22.readFileSync(configPath, "utf-8");
191653
+ const hcl = fs23.readFileSync(configPath, "utf-8");
191335
191654
  config = await parseConfig(hcl);
191336
191655
  } catch (err) {
191337
191656
  console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
@@ -191468,27 +191787,48 @@ async function execCommand(serviceName, command, instanceKey = "default") {
191468
191787
 
191469
191788
  // src/commands/psql.tsx
191470
191789
  import { spawn as spawn7 } from "child_process";
191471
- async function psqlCommand(databaseName, instanceKey = "default") {
191472
- const stateManager = new InstanceStateManager(process.cwd(), instanceKey);
191473
- await stateManager.cleanStaleState();
191474
- const existingInstances = await stateManager.getExistingInstances();
191475
- if (!existingInstances) {
191476
- console.error("Error: No running instances found. Run `specific dev` first.");
191790
+ import * as fs24 from "fs";
191791
+ import * as path22 from "path";
191792
+ async function psqlCommand(databaseName, instanceKey = "default", extraArgs = []) {
191793
+ let startedResources = [];
191794
+ let ownsInstances = false;
191795
+ let stateManager = null;
191796
+ const cleanup = async () => {
191797
+ if (ownsInstances) {
191798
+ for (const resource of startedResources) {
191799
+ await resource.stop().catch(() => {
191800
+ });
191801
+ }
191802
+ if (stateManager) {
191803
+ await stateManager.releaseOwnership().catch(() => {
191804
+ });
191805
+ }
191806
+ }
191807
+ };
191808
+ const configPath = path22.join(process.cwd(), "specific.hcl");
191809
+ if (!fs24.existsSync(configPath)) {
191810
+ console.error("Error: No specific.hcl found in current directory");
191811
+ process.exit(1);
191812
+ }
191813
+ let config;
191814
+ try {
191815
+ const hcl = fs24.readFileSync(configPath, "utf-8");
191816
+ config = await parseConfig(hcl);
191817
+ } catch (err) {
191818
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
191477
191819
  process.exit(1);
191478
191820
  }
191479
- const availableDatabases = Object.keys(existingInstances.databases);
191821
+ const availableDatabases = config.postgres.map((p) => p.name);
191480
191822
  if (availableDatabases.length === 0) {
191481
- console.error("Error: No databases found in running instances.");
191823
+ console.error("Error: No postgres databases defined in specific.hcl");
191482
191824
  process.exit(1);
191483
191825
  }
191484
191826
  let targetDb;
191827
+ if (databaseName && !availableDatabases.includes(databaseName)) {
191828
+ extraArgs = [databaseName, ...extraArgs];
191829
+ databaseName = void 0;
191830
+ }
191485
191831
  if (databaseName) {
191486
- if (!existingInstances.databases[databaseName]) {
191487
- console.error(
191488
- `Error: Database "${databaseName}" not found. Available: ${availableDatabases.join(", ")}`
191489
- );
191490
- process.exit(1);
191491
- }
191492
191832
  targetDb = databaseName;
191493
191833
  } else {
191494
191834
  if (availableDatabases.length > 1) {
@@ -191499,24 +191839,95 @@ async function psqlCommand(databaseName, instanceKey = "default") {
191499
191839
  }
191500
191840
  targetDb = availableDatabases[0];
191501
191841
  }
191502
- const dbState = existingInstances.databases[targetDb];
191503
- const child = spawn7("psql", ["-h", dbState.host, "-p", String(dbState.port), "-U", dbState.user, "-d", dbState.dbName], {
191842
+ stateManager = new InstanceStateManager(process.cwd(), instanceKey);
191843
+ await stateManager.cleanStaleState();
191844
+ const existingInstances = await stateManager.getExistingInstances();
191845
+ let dbState;
191846
+ if (existingInstances && existingInstances.databases[targetDb]) {
191847
+ const db = existingInstances.databases[targetDb];
191848
+ dbState = { host: db.host, port: db.port, user: db.user, password: db.password, dbName: db.dbName };
191849
+ } else {
191850
+ console.error("Starting database...");
191851
+ try {
191852
+ await stateManager.claimOwnership("psql");
191853
+ ownsInstances = true;
191854
+ } catch (err) {
191855
+ console.error(
191856
+ `Error: ${err instanceof Error ? err.message : String(err)}`
191857
+ );
191858
+ process.exit(1);
191859
+ }
191860
+ try {
191861
+ const result = await startResources({
191862
+ config,
191863
+ selection: {
191864
+ mode: "required",
191865
+ postgres: [targetDb],
191866
+ redis: [],
191867
+ storage: []
191868
+ },
191869
+ stateManager,
191870
+ dataDir: `.specific/keys/${instanceKey}/data`,
191871
+ portAllocator: new PortAllocator(),
191872
+ callbacks: {
191873
+ log: (msg) => console.error(msg)
191874
+ },
191875
+ startElectric: false
191876
+ });
191877
+ startedResources = result.startedResources;
191878
+ const resource = result.resources.get(targetDb);
191879
+ if (!resource) {
191880
+ console.error(`Error: Failed to start database "${targetDb}"`);
191881
+ await cleanup();
191882
+ process.exit(1);
191883
+ }
191884
+ dbState = {
191885
+ host: resource.host,
191886
+ port: resource.port,
191887
+ user: resource.user,
191888
+ password: resource.password,
191889
+ dbName: resource.dbName
191890
+ };
191891
+ } catch (err) {
191892
+ console.error(
191893
+ `Error: Failed to start database: ${err instanceof Error ? err.message : String(err)}`
191894
+ );
191895
+ await cleanup();
191896
+ process.exit(1);
191897
+ }
191898
+ }
191899
+ const binary = await ensureBinary(postgresBinary);
191900
+ const psqlPath = binary.executables["psql"];
191901
+ const libraryEnv = getLibraryEnv(binary);
191902
+ let child = null;
191903
+ const handleSignal = async (signal) => {
191904
+ if (child && !child.killed) {
191905
+ child.kill(signal);
191906
+ }
191907
+ await cleanup();
191908
+ };
191909
+ process.on("SIGINT", () => handleSignal("SIGINT"));
191910
+ process.on("SIGTERM", () => handleSignal("SIGTERM"));
191911
+ child = spawn7(psqlPath, ["-h", dbState.host, "-p", String(dbState.port), "-U", dbState.user, "-d", dbState.dbName, ...extraArgs], {
191504
191912
  cwd: process.cwd(),
191505
191913
  env: {
191506
191914
  ...process.env,
191915
+ ...libraryEnv,
191507
191916
  PGPASSWORD: dbState.password
191508
191917
  },
191509
191918
  stdio: "inherit"
191510
191919
  });
191511
- child.on("exit", (code, signal) => {
191920
+ child.on("exit", async (code, signal) => {
191921
+ await cleanup();
191512
191922
  if (signal) {
191513
191923
  process.kill(process.pid, signal);
191514
191924
  } else {
191515
191925
  process.exit(code ?? 0);
191516
191926
  }
191517
191927
  });
191518
- child.on("error", (err) => {
191928
+ child.on("error", async (err) => {
191519
191929
  console.error(`Error: Failed to start psql: ${err.message}`);
191930
+ await cleanup();
191520
191931
  process.exit(1);
191521
191932
  });
191522
191933
  }
@@ -191525,21 +191936,21 @@ async function psqlCommand(databaseName, instanceKey = "default") {
191525
191936
  import React8, { useState as useState7, useEffect as useEffect5 } from "react";
191526
191937
  import { render as render6, Text as Text8, Box as Box8 } from "ink";
191527
191938
  import Spinner6 from "ink-spinner";
191528
- import * as fs23 from "fs";
191529
- import * as path21 from "path";
191939
+ import * as fs25 from "fs";
191940
+ import * as path23 from "path";
191530
191941
  function CleanUI({ instanceKey }) {
191531
191942
  const [state, setState] = useState7({ status: "checking" });
191532
191943
  useEffect5(() => {
191533
191944
  async function clean() {
191534
191945
  const projectRoot = process.cwd();
191535
- const specificDir = path21.join(projectRoot, ".specific");
191536
- if (!fs23.existsSync(specificDir)) {
191946
+ const specificDir = path23.join(projectRoot, ".specific");
191947
+ if (!fs25.existsSync(specificDir)) {
191537
191948
  setState({ status: "nothing" });
191538
191949
  return;
191539
191950
  }
191540
191951
  if (instanceKey) {
191541
- const keyDir = path21.join(specificDir, "keys", instanceKey);
191542
- if (!fs23.existsSync(keyDir)) {
191952
+ const keyDir = path23.join(specificDir, "keys", instanceKey);
191953
+ if (!fs25.existsSync(keyDir)) {
191543
191954
  setState({ status: "nothing" });
191544
191955
  return;
191545
191956
  }
@@ -191555,7 +191966,7 @@ function CleanUI({ instanceKey }) {
191555
191966
  await stateManager.cleanStaleState();
191556
191967
  setState({ status: "cleaning" });
191557
191968
  try {
191558
- fs23.rmSync(keyDir, { recursive: true, force: true });
191969
+ fs25.rmSync(keyDir, { recursive: true, force: true });
191559
191970
  setState({ status: "success" });
191560
191971
  } catch (err) {
191561
191972
  setState({
@@ -191564,10 +191975,10 @@ function CleanUI({ instanceKey }) {
191564
191975
  });
191565
191976
  }
191566
191977
  } 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()
191978
+ const keysDir = path23.join(specificDir, "keys");
191979
+ if (fs25.existsSync(keysDir)) {
191980
+ const keys = fs25.readdirSync(keysDir).filter(
191981
+ (f) => fs25.statSync(path23.join(keysDir, f)).isDirectory()
191571
191982
  );
191572
191983
  for (const key of keys) {
191573
191984
  const stateManager2 = new InstanceStateManager(projectRoot, key);
@@ -191592,7 +192003,7 @@ function CleanUI({ instanceKey }) {
191592
192003
  }
191593
192004
  setState({ status: "cleaning" });
191594
192005
  try {
191595
- fs23.rmSync(specificDir, { recursive: true, force: true });
192006
+ fs25.rmSync(specificDir, { recursive: true, force: true });
191596
192007
  setState({ status: "success" });
191597
192008
  } catch (err) {
191598
192009
  setState({
@@ -191679,13 +192090,13 @@ function logoutCommand() {
191679
192090
  var program = new Command();
191680
192091
  var env = "production";
191681
192092
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
191682
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.47").enablePositionalOptions();
191683
- program.command("init").description("Initialize project for use with a coding agent").action(initCommand);
192093
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.49").enablePositionalOptions();
192094
+ program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").action((options2) => initCommand(options2));
191684
192095
  program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
191685
192096
  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) => {
192097
+ 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
192098
  const key = options2.key ?? getDefaultKey();
191688
- devCommand(key);
192099
+ devCommand(key, options2.tunnel ?? false);
191689
192100
  });
191690
192101
  program.command("deploy [environment]").description("Deploy to Specific infrastructure").option("--skip-build-test", "Skip local build testing before deploy").action((environment, options2) => {
191691
192102
  deployCommand(environment, options2);
@@ -191695,9 +192106,10 @@ program.command("exec <service> [args...]").description("Run a one-off command w
191695
192106
  const key = options2.key ?? getDefaultKey();
191696
192107
  await execCommand(service, filteredArgs, key);
191697
192108
  });
191698
- program.command("psql [database]").description("Connect to a running Postgres database").option("-k, --key <key>", "Dev environment namespace (auto-detected from git worktree if not specified)").action((database, options2) => {
192109
+ program.command("psql [database] [args...]").description("Connect to a Postgres database").option("-k, --key <key>", "Dev environment namespace (auto-detected from git worktree if not specified)").passThroughOptions().action((database, args, options2) => {
192110
+ const filteredArgs = args[0] === "--" ? args.slice(1) : args;
191699
192111
  const key = options2.key ?? getDefaultKey();
191700
- psqlCommand(database, key);
192112
+ psqlCommand(database, key, filteredArgs);
191701
192113
  });
191702
192114
  program.command("clean").description("Remove .specific directory for a clean slate").option("-k, --key <key>", "Clean only the specified dev environment key").action((options2) => {
191703
192115
  cleanCommand(options2.key);