@specific.dev/cli 0.1.125 → 0.1.127

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 (67) hide show
  1. package/dist/admin/404/index.html +1 -1
  2. package/dist/admin/404.html +1 -1
  3. package/dist/admin/__next.!KGRlZmF1bHQp.__PAGE__.txt +1 -1
  4. package/dist/admin/__next.!KGRlZmF1bHQp.txt +1 -1
  5. package/dist/admin/__next._full.txt +1 -1
  6. package/dist/admin/__next._head.txt +1 -1
  7. package/dist/admin/__next._index.txt +1 -1
  8. package/dist/admin/__next._tree.txt +1 -1
  9. package/dist/admin/_not-found/__next._full.txt +1 -1
  10. package/dist/admin/_not-found/__next._head.txt +1 -1
  11. package/dist/admin/_not-found/__next._index.txt +1 -1
  12. package/dist/admin/_not-found/__next._not-found.__PAGE__.txt +1 -1
  13. package/dist/admin/_not-found/__next._not-found.txt +1 -1
  14. package/dist/admin/_not-found/__next._tree.txt +1 -1
  15. package/dist/admin/_not-found/index.html +1 -1
  16. package/dist/admin/_not-found/index.txt +1 -1
  17. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.__PAGE__.txt +1 -1
  18. package/dist/admin/databases/__next.!KGRlZmF1bHQp.databases.txt +1 -1
  19. package/dist/admin/databases/__next.!KGRlZmF1bHQp.txt +1 -1
  20. package/dist/admin/databases/__next._full.txt +1 -1
  21. package/dist/admin/databases/__next._head.txt +1 -1
  22. package/dist/admin/databases/__next._index.txt +1 -1
  23. package/dist/admin/databases/__next._tree.txt +1 -1
  24. package/dist/admin/databases/index.html +1 -1
  25. package/dist/admin/databases/index.txt +1 -1
  26. package/dist/admin/fullscreen/__next._full.txt +1 -1
  27. package/dist/admin/fullscreen/__next._head.txt +1 -1
  28. package/dist/admin/fullscreen/__next._index.txt +1 -1
  29. package/dist/admin/fullscreen/__next._tree.txt +1 -1
  30. package/dist/admin/fullscreen/__next.fullscreen.__PAGE__.txt +1 -1
  31. package/dist/admin/fullscreen/__next.fullscreen.txt +1 -1
  32. package/dist/admin/fullscreen/databases/__next._full.txt +1 -1
  33. package/dist/admin/fullscreen/databases/__next._head.txt +1 -1
  34. package/dist/admin/fullscreen/databases/__next._index.txt +1 -1
  35. package/dist/admin/fullscreen/databases/__next._tree.txt +1 -1
  36. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.__PAGE__.txt +1 -1
  37. package/dist/admin/fullscreen/databases/__next.fullscreen.databases.txt +1 -1
  38. package/dist/admin/fullscreen/databases/__next.fullscreen.txt +1 -1
  39. package/dist/admin/fullscreen/databases/index.html +1 -1
  40. package/dist/admin/fullscreen/databases/index.txt +1 -1
  41. package/dist/admin/fullscreen/index.html +1 -1
  42. package/dist/admin/fullscreen/index.txt +1 -1
  43. package/dist/admin/index.html +1 -1
  44. package/dist/admin/index.txt +1 -1
  45. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.__PAGE__.txt +1 -1
  46. package/dist/admin/mail/__next.!KGRlZmF1bHQp.mail.txt +1 -1
  47. package/dist/admin/mail/__next.!KGRlZmF1bHQp.txt +1 -1
  48. package/dist/admin/mail/__next._full.txt +1 -1
  49. package/dist/admin/mail/__next._head.txt +1 -1
  50. package/dist/admin/mail/__next._index.txt +1 -1
  51. package/dist/admin/mail/__next._tree.txt +1 -1
  52. package/dist/admin/mail/index.html +1 -1
  53. package/dist/admin/mail/index.txt +1 -1
  54. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.txt +1 -1
  55. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.__PAGE__.txt +1 -1
  56. package/dist/admin/workflows/__next.!KGRlZmF1bHQp.workflows.txt +1 -1
  57. package/dist/admin/workflows/__next._full.txt +1 -1
  58. package/dist/admin/workflows/__next._head.txt +1 -1
  59. package/dist/admin/workflows/__next._index.txt +1 -1
  60. package/dist/admin/workflows/__next._tree.txt +1 -1
  61. package/dist/admin/workflows/index.html +1 -1
  62. package/dist/admin/workflows/index.txt +1 -1
  63. package/dist/cli.js +447 -87
  64. package/package.json +4 -4
  65. /package/dist/admin/_next/static/{r3veCvOuDWyK-S0rsl-XQ → tWkoycW-NhVqIuvprR9OV}/_buildManifest.js +0 -0
  66. /package/dist/admin/_next/static/{r3veCvOuDWyK-S0rsl-XQ → tWkoycW-NhVqIuvprR9OV}/_clientMiddlewareManifest.json +0 -0
  67. /package/dist/admin/_next/static/{r3veCvOuDWyK-S0rsl-XQ → tWkoycW-NhVqIuvprR9OV}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -185179,25 +185179,26 @@ var chokidar_default = { watch, FSWatcher };
185179
185179
  var import_code_frame = __toESM(require_lib2(), 1);
185180
185180
  import * as fs5 from "fs";
185181
185181
  import * as path5 from "path";
185182
- import * as net from "net";
185182
+ import * as net2 from "net";
185183
185183
  import { spawn } from "child_process";
185184
185184
  import * as fs4 from "fs";
185185
185185
  import * as path4 from "path";
185186
185186
  import * as os2 from "os";
185187
185187
  import { createReadStream } from "fs";
185188
185188
  import { createTarExtractor, extractTo } from "tar-vern";
185189
+ import * as net from "net";
185190
+ import { execFile } from "child_process";
185191
+ import { promisify } from "util";
185189
185192
  import { spawn as spawn2 } from "child_process";
185190
185193
  import { readFile, writeFile } from "fs/promises";
185191
185194
  import { existsSync as existsSync5 } from "fs";
185192
185195
  import * as fs6 from "fs";
185193
185196
  import * as path6 from "path";
185194
- import { execFile } from "child_process";
185195
- import { promisify } from "util";
185196
185197
  import * as http from "http";
185197
185198
  import * as fs7 from "fs";
185198
185199
  import * as path7 from "path";
185199
185200
  import { fileURLToPath } from "url";
185200
- import * as net2 from "net";
185201
+ import * as net3 from "net";
185201
185202
  import * as fs8 from "fs";
185202
185203
  import * as path9 from "path";
185203
185204
  import { spawn as spawn3 } from "child_process";
@@ -185207,7 +185208,7 @@ import * as path8 from "path";
185207
185208
  import * as crypto from "crypto";
185208
185209
  import * as http2 from "http";
185209
185210
  import * as crypto2 from "crypto";
185210
- import * as net3 from "net";
185211
+ import * as net4 from "net";
185211
185212
  import * as fs9 from "fs";
185212
185213
  import * as path10 from "path";
185213
185214
  import { spawn as spawn4 } from "child_process";
@@ -185217,14 +185218,14 @@ import * as path11 from "path";
185217
185218
  import { spawnSync } from "child_process";
185218
185219
  import * as fs11 from "fs";
185219
185220
  import * as path12 from "path";
185220
- import * as net4 from "net";
185221
- import { spawn as spawn5 } from "child_process";
185222
185221
  import * as net5 from "net";
185222
+ import { spawn as spawn5 } from "child_process";
185223
+ import * as net6 from "net";
185223
185224
  import * as fs12 from "fs";
185224
185225
  import * as path13 from "path";
185225
185226
  import { generateSlug } from "random-word-slugs";
185226
185227
  import { EventEmitter as EventEmitter2 } from "node:events";
185227
- import * as net6 from "node:net";
185228
+ import * as net7 from "node:net";
185228
185229
  import { EventEmitter as EventEmitter22 } from "events";
185229
185230
  import { execSync as execSync2 } from "child_process";
185230
185231
  import * as path15 from "path";
@@ -369275,14 +369276,22 @@ var temporalBinary = {
369275
369276
  stripComponents: 0,
369276
369277
  executables: ["temporal"]
369277
369278
  };
369278
- function killProcess(proc) {
369279
+ function killProcess(proc, opts = {}) {
369279
369280
  return new Promise((resolve52) => {
369280
369281
  if (proc.killed || proc.exitCode !== null) {
369281
369282
  resolve52();
369282
369283
  return;
369283
369284
  }
369284
369285
  proc.once("exit", () => resolve52());
369285
- proc.kill("SIGKILL");
369286
+ if (opts.detached && proc.pid) {
369287
+ try {
369288
+ process.kill(-proc.pid, "SIGKILL");
369289
+ } catch {
369290
+ proc.kill("SIGKILL");
369291
+ }
369292
+ } else {
369293
+ proc.kill("SIGKILL");
369294
+ }
369286
369295
  });
369287
369296
  }
369288
369297
  function killProcessGroup(pid) {
@@ -369291,6 +369300,143 @@ function killProcessGroup(pid) {
369291
369300
  } catch {
369292
369301
  }
369293
369302
  }
369303
+ var execFileAsync = promisify(execFile);
369304
+ function isPortInUse(host, port) {
369305
+ return new Promise((resolve52) => {
369306
+ const socket = new net.Socket();
369307
+ socket.setTimeout(1e3);
369308
+ socket.on("connect", () => {
369309
+ socket.destroy();
369310
+ resolve52(true);
369311
+ });
369312
+ socket.on("timeout", () => {
369313
+ socket.destroy();
369314
+ resolve52(false);
369315
+ });
369316
+ socket.on("error", () => {
369317
+ socket.destroy();
369318
+ resolve52(false);
369319
+ });
369320
+ socket.connect(port, host);
369321
+ });
369322
+ }
369323
+ async function waitForPortFree(host, port, timeoutMs = 3e3) {
369324
+ const deadline = Date.now() + timeoutMs;
369325
+ while (Date.now() < deadline) {
369326
+ if (!await isPortInUse(host, port)) return true;
369327
+ await new Promise((r) => setTimeout(r, 100));
369328
+ }
369329
+ return !await isPortInUse(host, port);
369330
+ }
369331
+ async function getListenersOnPort(port) {
369332
+ let pids = [];
369333
+ try {
369334
+ const { stdout } = await execFileAsync(
369335
+ "lsof",
369336
+ ["-i", `:${port}`, "-sTCP:LISTEN", "-t"],
369337
+ { timeout: 5e3 }
369338
+ );
369339
+ pids = stdout.trim().split("\n").map((l) => parseInt(l.trim(), 10)).filter((n) => !isNaN(n));
369340
+ } catch {
369341
+ return [];
369342
+ }
369343
+ const results = [];
369344
+ for (const pid of pids) {
369345
+ results.push({ pid, command: await resolveCommand(pid) });
369346
+ }
369347
+ return results;
369348
+ }
369349
+ async function resolveCommand(pid) {
369350
+ try {
369351
+ const { stdout } = await execFileAsync(
369352
+ "ps",
369353
+ ["-p", String(pid), "-o", "command="],
369354
+ { timeout: 1e3 }
369355
+ );
369356
+ return stdout.trim();
369357
+ } catch {
369358
+ return "";
369359
+ }
369360
+ }
369361
+ async function reclaimSpecificOrphanOnPort(port) {
369362
+ const listeners = await getListenersOnPort(port);
369363
+ let killed = false;
369364
+ for (const { pid, command } of listeners) {
369365
+ if (!isOwnedBySpecific(command)) continue;
369366
+ try {
369367
+ process.kill(-pid, "SIGKILL");
369368
+ killed = true;
369369
+ } catch {
369370
+ try {
369371
+ process.kill(pid, "SIGKILL");
369372
+ killed = true;
369373
+ } catch {
369374
+ }
369375
+ }
369376
+ }
369377
+ return killed;
369378
+ }
369379
+ function isOwnedBySpecific(argv) {
369380
+ return argv.includes("/.specific/bin/") || argv.includes("/.burrito/electric_");
369381
+ }
369382
+ async function killSpecificProcessesReferencing(needle) {
369383
+ if (!needle) return 0;
369384
+ let stdout = "";
369385
+ try {
369386
+ const result = await execFileAsync("ps", ["-A", "-o", "pid=,command="], {
369387
+ timeout: 5e3,
369388
+ maxBuffer: 10 * 1024 * 1024
369389
+ });
369390
+ stdout = result.stdout;
369391
+ } catch {
369392
+ return 0;
369393
+ }
369394
+ const self2 = process.pid;
369395
+ const candidates = [];
369396
+ for (const raw of stdout.split("\n")) {
369397
+ const line = raw.trim();
369398
+ if (!line) continue;
369399
+ const space = line.indexOf(" ");
369400
+ if (space <= 0) continue;
369401
+ const pid = parseInt(line.slice(0, space), 10);
369402
+ if (isNaN(pid) || pid === self2) continue;
369403
+ const command = line.slice(space + 1);
369404
+ if (!isOwnedBySpecific(command)) continue;
369405
+ if (command.includes(needle) || await processOpensPathUnder(pid, needle)) {
369406
+ candidates.push(pid);
369407
+ }
369408
+ }
369409
+ let killed = 0;
369410
+ for (const pid of candidates) {
369411
+ try {
369412
+ process.kill(-pid, "SIGKILL");
369413
+ killed++;
369414
+ } catch {
369415
+ try {
369416
+ process.kill(pid, "SIGKILL");
369417
+ killed++;
369418
+ } catch {
369419
+ }
369420
+ }
369421
+ }
369422
+ return killed;
369423
+ }
369424
+ async function processOpensPathUnder(pid, prefix) {
369425
+ try {
369426
+ const { stdout } = await execFileAsync("lsof", ["-p", String(pid), "-F", "n"], {
369427
+ timeout: 3e3,
369428
+ maxBuffer: 5 * 1024 * 1024
369429
+ });
369430
+ for (const raw of stdout.split("\n")) {
369431
+ if (raw.length > 1 && raw[0] === "n" && raw.slice(1).startsWith(prefix)) {
369432
+ return true;
369433
+ }
369434
+ }
369435
+ } catch {
369436
+ return false;
369437
+ }
369438
+ return false;
369439
+ }
369294
369440
  async function startPostgres(pg, port, dataDir, onProgress) {
369295
369441
  const binary = await ensureBinary(postgresBinary, void 0, onProgress);
369296
369442
  const dbDataPath = path5.join(process.cwd(), dataDir, pg.name);
@@ -369299,6 +369445,7 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369299
369445
  const password = "postgres";
369300
369446
  const libraryEnv = getLibraryEnv(binary);
369301
369447
  const env2 = { ...process.env, ...libraryEnv };
369448
+ reclaimOrphanedPostmaster(dbDataPath);
369302
369449
  const pgConfPath = path5.join(dbDataPath, "postgresql.conf");
369303
369450
  const dataValid = fs5.existsSync(pgConfPath);
369304
369451
  if (!dataValid) {
@@ -369314,6 +369461,10 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369314
369461
  } catch {
369315
369462
  }
369316
369463
  }
369464
+ if (await isPortInUse(host, port)) {
369465
+ await reclaimSpecificOrphanOnPort(port);
369466
+ await waitForPortFree(host, port, 1e3);
369467
+ }
369317
369468
  const postgres = spawn(
369318
369469
  binary.executables["postgres"],
369319
369470
  [
@@ -369332,9 +369483,11 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369332
369483
  ],
369333
369484
  {
369334
369485
  stdio: ["ignore", "pipe", "pipe"],
369335
- env: env2
369486
+ env: env2,
369487
+ detached: true
369336
369488
  }
369337
369489
  );
369490
+ postgres.unref();
369338
369491
  pipeProcess("postgres", postgres);
369339
369492
  await Promise.race([
369340
369493
  waitForTcpPort(host, port),
@@ -369358,21 +369511,28 @@ async function startPostgres(pg, port, dataDir, onProgress) {
369358
369511
  dbName: pg.name,
369359
369512
  url: `postgres://${user}:${password}@${host}:${port}/${pg.name}`,
369360
369513
  pid: postgres.pid,
369514
+ detached: true,
369361
369515
  async stop() {
369362
- return killProcess(postgres);
369516
+ return killProcess(postgres, { detached: true });
369363
369517
  }
369364
369518
  };
369365
369519
  }
369366
369520
  async function startRedis(redis, port, onProgress) {
369367
369521
  const binary = await ensureBinary(redisBinary, void 0, onProgress);
369368
369522
  const host = "127.0.0.1";
369523
+ if (await isPortInUse(host, port)) {
369524
+ await reclaimSpecificOrphanOnPort(port);
369525
+ await waitForPortFree(host, port, 1e3);
369526
+ }
369369
369527
  const redisProc = spawn(
369370
369528
  binary.executables["redis-server"],
369371
369529
  ["--port", String(port), "--bind", host, "--daemonize", "no"],
369372
369530
  {
369373
- stdio: ["ignore", "pipe", "pipe"]
369531
+ stdio: ["ignore", "pipe", "pipe"],
369532
+ detached: true
369374
369533
  }
369375
369534
  );
369535
+ redisProc.unref();
369376
369536
  pipeProcess("redis", redisProc);
369377
369537
  await waitForTcpPort(host, port);
369378
369538
  return {
@@ -369385,8 +369545,9 @@ async function startRedis(redis, port, onProgress) {
369385
369545
  dbName: redis.name,
369386
369546
  url: `redis://${host}:${port}`,
369387
369547
  pid: redisProc.pid,
369548
+ detached: true,
369388
369549
  async stop() {
369389
- return killProcess(redisProc);
369550
+ return killProcess(redisProc, { detached: true });
369390
369551
  }
369391
369552
  };
369392
369553
  }
@@ -369474,6 +369635,39 @@ async function createPostgresDatabase(postgresPath, dataDir, dbName, env2) {
369474
369635
  proc.stdin?.end();
369475
369636
  });
369476
369637
  }
369638
+ function reclaimOrphanedPostmaster(dbDataPath) {
369639
+ const pidFile = path5.join(dbDataPath, "postmaster.pid");
369640
+ if (!fs5.existsSync(pidFile)) return;
369641
+ let pid;
369642
+ try {
369643
+ const firstLine = fs5.readFileSync(pidFile, "utf-8").split("\n")[0] ?? "";
369644
+ pid = parseInt(firstLine.trim(), 10);
369645
+ } catch {
369646
+ return;
369647
+ }
369648
+ if (isNaN(pid) || pid <= 0) return;
369649
+ let alive = false;
369650
+ try {
369651
+ process.kill(pid, 0);
369652
+ alive = true;
369653
+ } catch (e) {
369654
+ alive = e.code !== "ESRCH";
369655
+ }
369656
+ if (alive) {
369657
+ try {
369658
+ process.kill(-pid, "SIGKILL");
369659
+ } catch {
369660
+ try {
369661
+ process.kill(pid, "SIGKILL");
369662
+ } catch {
369663
+ }
369664
+ }
369665
+ }
369666
+ try {
369667
+ fs5.unlinkSync(pidFile);
369668
+ } catch {
369669
+ }
369670
+ }
369477
369671
  async function waitForTcpPort(host, port, timeoutMs = 3e4) {
369478
369672
  const startTime = Date.now();
369479
369673
  while (Date.now() - startTime < timeoutMs) {
@@ -369487,7 +369681,7 @@ async function waitForTcpPort(host, port, timeoutMs = 3e4) {
369487
369681
  }
369488
369682
  function checkTcpPort(host, port) {
369489
369683
  return new Promise((resolve52) => {
369490
- const socket = new net.Socket();
369684
+ const socket = new net2.Socket();
369491
369685
  socket.setTimeout(1e3);
369492
369686
  socket.on("connect", () => {
369493
369687
  socket.destroy();
@@ -369882,31 +370076,59 @@ function resolveEnv(env2, resources, secrets, configs, servicePort, serviceEndpo
369882
370076
  }
369883
370077
  return resolved;
369884
370078
  }
369885
- function resolveEnvForExec(env2, resources, secrets, configs) {
370079
+ function resolveEnvForExec(env2, resources, secrets, configs, runningContext) {
369886
370080
  if (!env2) {
369887
370081
  return { env: {}, missingSecrets: [], missingConfigs: [] };
369888
370082
  }
369889
370083
  const resolved = {};
369890
370084
  const missingSecrets = [];
369891
370085
  const missingConfigs = [];
370086
+ const canResolveRef = (ref) => {
370087
+ if (!runningContext) return false;
370088
+ switch (ref.type) {
370089
+ case "port":
370090
+ return runningContext.servicePort !== void 0;
370091
+ case "endpoint":
370092
+ return runningContext.currentServicePorts !== void 0;
370093
+ case "service": {
370094
+ if (!runningContext.serviceEndpoints) return false;
370095
+ if (ref.attribute === "public_url" && !runningContext.publicUrls) return false;
370096
+ return true;
370097
+ }
370098
+ case "volume":
370099
+ return false;
370100
+ default:
370101
+ return true;
370102
+ }
370103
+ };
370104
+ const isSkippableType = (type) => type === "port" || type === "endpoint" || type === "service" || type === "volume";
369892
370105
  for (const [key, value] of Object.entries(env2)) {
369893
370106
  if (typeof value === "string") {
369894
370107
  resolved[key] = value;
369895
370108
  continue;
369896
370109
  }
369897
- if (value.type === "port" || value.type === "endpoint" || value.type === "service" || value.type === "volume") {
370110
+ if (isSkippableType(value.type) && !canResolveRef(value)) {
369898
370111
  continue;
369899
370112
  }
369900
370113
  if (value.type === "interpolated") {
369901
- const hasSkippableRef = value.parts.some(
369902
- (part) => part.type === "ref" && (part.ref.type === "port" || part.ref.type === "endpoint" || part.ref.type === "service" || part.ref.type === "volume")
370114
+ const hasUnresolvableRef = value.parts.some(
370115
+ (part) => part.type === "ref" && isSkippableType(part.ref.type) && !canResolveRef(part.ref)
369903
370116
  );
369904
- if (hasSkippableRef) {
370117
+ if (hasUnresolvableRef) {
369905
370118
  continue;
369906
370119
  }
369907
370120
  }
369908
370121
  try {
369909
- resolved[key] = resolveEnvValue(value, resources, secrets, configs);
370122
+ resolved[key] = resolveEnvValue(
370123
+ value,
370124
+ resources,
370125
+ secrets,
370126
+ configs,
370127
+ runningContext?.servicePort,
370128
+ runningContext?.serviceEndpoints,
370129
+ runningContext?.currentServicePorts,
370130
+ runningContext?.publicUrls
370131
+ );
369910
370132
  } catch (err) {
369911
370133
  if (err instanceof MissingSecretError) {
369912
370134
  missingSecrets.push({ name: err.secretName, envVar: key });
@@ -369987,7 +370209,6 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
369987
370209
  }
369988
370210
  };
369989
370211
  }
369990
- var execFileAsync = promisify(execFile);
369991
370212
  var InstanceStateManager = class {
369992
370213
  stateDir;
369993
370214
  statePath;
@@ -370079,8 +370300,20 @@ var InstanceStateManager = class {
370079
370300
  releaseLock();
370080
370301
  }
370081
370302
  }
370303
+ /**
370304
+ * Kill every Specific-owned process (postgres, redis, electric, temporal,
370305
+ * drizzle-gateway) whose command line references this key's data
370306
+ * directory. This is the safety net for when state.json is missing or
370307
+ * incomplete — the port allocator can pick a different block on restart,
370308
+ * so port-based cleanup alone isn't enough. We identify orphans by their
370309
+ * data-dir/config-dir argument, which is stable across runs.
370310
+ */
370311
+ async sweepKeyDirOrphans() {
370312
+ await killSpecificProcessesReferencing(this.stateDir);
370313
+ }
370082
370314
  async cleanStaleState() {
370083
370315
  if (!fs6.existsSync(this.statePath)) {
370316
+ await this.sweepKeyDirOrphans();
370084
370317
  return false;
370085
370318
  }
370086
370319
  const releaseLock = await this.acquireLock();
@@ -370089,6 +370322,7 @@ var InstanceStateManager = class {
370089
370322
  const state = JSON.parse(content);
370090
370323
  if (!this.isProcessRunning(state.owner.pid)) {
370091
370324
  await this.killOrphanedProcesses(state);
370325
+ await this.sweepKeyDirOrphans();
370092
370326
  fs6.unlinkSync(this.statePath);
370093
370327
  return true;
370094
370328
  }
@@ -370118,18 +370352,22 @@ var InstanceStateManager = class {
370118
370352
  }
370119
370353
  }
370120
370354
  for (const db of Object.values(state.databases)) {
370121
- if (db.syncUrl) {
370122
- try {
370123
- const url = new URL(db.syncUrl);
370124
- const port = parseInt(url.port, 10);
370125
- if (!isNaN(port)) {
370126
- const pid = await findPidOnPort(port);
370127
- if (pid !== void 0) {
370128
- process.kill(pid, "SIGKILL");
370129
- }
370130
- }
370131
- } catch {
370132
- }
370355
+ killTrackedProcess(db.pid, db.detached, this.isProcessRunning.bind(this));
370356
+ if (db.port) {
370357
+ await reclaimSpecificOrphanOnPort(db.port);
370358
+ }
370359
+ }
370360
+ for (const aux of Object.values(state.electric ?? {})) {
370361
+ killTrackedProcess(aux.pid, aux.detached, this.isProcessRunning.bind(this));
370362
+ if (aux.port) {
370363
+ await reclaimSpecificOrphanOnPort(aux.port);
370364
+ }
370365
+ }
370366
+ if (state.drizzleGateway) {
370367
+ const dg = state.drizzleGateway;
370368
+ killTrackedProcess(dg.pid, dg.detached, this.isProcessRunning.bind(this));
370369
+ if (dg.port) {
370370
+ await reclaimSpecificOrphanOnPort(dg.port);
370133
370371
  }
370134
370372
  }
370135
370373
  }
@@ -370200,6 +370438,45 @@ var InstanceStateManager = class {
370200
370438
  releaseLock();
370201
370439
  }
370202
370440
  }
370441
+ async registerElectric(databaseName, info) {
370442
+ if (!this.ownsInstances) {
370443
+ throw new Error("Cannot register Electric: not the owner");
370444
+ }
370445
+ const releaseLock = await this.acquireLock();
370446
+ try {
370447
+ const state = this.readState();
370448
+ state.electric = { ...state.electric ?? {}, [databaseName]: info };
370449
+ this.writeStateAtomic(state);
370450
+ } finally {
370451
+ releaseLock();
370452
+ }
370453
+ }
370454
+ async registerDrizzleGateway(info) {
370455
+ if (!this.ownsInstances) {
370456
+ throw new Error("Cannot register Drizzle Gateway: not the owner");
370457
+ }
370458
+ const releaseLock = await this.acquireLock();
370459
+ try {
370460
+ const state = this.readState();
370461
+ state.drizzleGateway = info;
370462
+ this.writeStateAtomic(state);
370463
+ } finally {
370464
+ releaseLock();
370465
+ }
370466
+ }
370467
+ async setPublicUrls(publicUrls) {
370468
+ if (!this.ownsInstances) {
370469
+ throw new Error("Cannot set public URLs: not the owner");
370470
+ }
370471
+ const releaseLock = await this.acquireLock();
370472
+ try {
370473
+ const state = this.readState();
370474
+ state.publicUrls = publicUrls;
370475
+ this.writeStateAtomic(state);
370476
+ } finally {
370477
+ releaseLock();
370478
+ }
370479
+ }
370203
370480
  async releaseOwnership() {
370204
370481
  if (!this.ownsInstances) {
370205
370482
  return;
@@ -370225,17 +370502,15 @@ var InstanceStateManager = class {
370225
370502
  fs6.renameSync(tmpPath, this.statePath);
370226
370503
  }
370227
370504
  };
370228
- async function findPidOnPort(port) {
370505
+ function killTrackedProcess(pid, detached, isProcessRunning) {
370506
+ if (!pid || !isProcessRunning(pid)) return;
370229
370507
  try {
370230
- const { stdout } = await execFileAsync(
370231
- "lsof",
370232
- ["-i", `:${port}`, "-t", "-sTCP:LISTEN"],
370233
- { timeout: 5e3 }
370234
- );
370235
- const pid = parseInt(stdout.trim().split("\n")[0], 10);
370236
- return isNaN(pid) ? void 0 : pid;
370508
+ if (detached) {
370509
+ process.kill(-pid, "SIGKILL");
370510
+ } else {
370511
+ process.kill(pid, "SIGKILL");
370512
+ }
370237
370513
  } catch {
370238
- return void 0;
370239
370514
  }
370240
370515
  }
370241
370516
  var __dirname = path7.dirname(fileURLToPath(import.meta.url));
@@ -370533,21 +370808,15 @@ async function startElectric(postgres, port, dataDir, options2) {
370533
370808
  );
370534
370809
  const secret = generateRandomString(32);
370535
370810
  const host = "127.0.0.1";
370536
- if (await checkTcpPort2(host, port)) {
370537
- let freed = false;
370538
- for (let i = 0; i < 30; i++) {
370539
- await sleep2(100);
370540
- if (!await checkTcpPort2(host, port)) {
370541
- freed = true;
370542
- break;
370543
- }
370544
- }
370811
+ if (await isPortInUse(host, port)) {
370812
+ await reclaimSpecificOrphanOnPort(port);
370813
+ const freed = await waitForPortFree(host, port, 3e3);
370545
370814
  if (!freed) {
370546
370815
  throw new Error(
370547
- `Electric port ${port} is already in use. This may be an orphaned process from a previous session \u2014 find it with \`lsof -i :${port}\` and kill it, then retry.`
370816
+ `Electric port ${port} is already in use by a non-Specific process. Find it with \`lsof -i :${port}\` and kill it, then retry.`
370548
370817
  );
370549
370818
  }
370550
- writeLog("electric", `Port ${port} was occupied but is now free`);
370819
+ writeLog("electric", `Port ${port} was occupied but has been reclaimed`);
370551
370820
  }
370552
370821
  const storageDir = path9.join(process.cwd(), dataDir, `electric-${postgres.name}`);
370553
370822
  fs8.rmSync(storageDir, { recursive: true, force: true });
@@ -370559,6 +370828,7 @@ async function startElectric(postgres, port, dataDir, options2) {
370559
370828
  writeLog("electric", `Binary: ${binary.executables["electric"]}`);
370560
370829
  const electric = spawn3(binary.executables["electric"], [], {
370561
370830
  stdio: ["ignore", "pipe", "pipe"],
370831
+ detached: true,
370562
370832
  env: {
370563
370833
  ...process.env,
370564
370834
  DATABASE_URL: postgres.url,
@@ -370569,16 +370839,18 @@ async function startElectric(postgres, port, dataDir, options2) {
370569
370839
  ELECTRIC_STORAGE: "MEMORY"
370570
370840
  }
370571
370841
  });
370842
+ electric.unref();
370572
370843
  pipeProcess("electric", electric);
370573
370844
  await waitForTcpPort2(host, port);
370574
370845
  return {
370575
370846
  databaseName: postgres.name,
370576
370847
  pid: electric.pid,
370848
+ detached: true,
370577
370849
  port,
370578
370850
  url: `http://${host}:${port}`,
370579
370851
  secret,
370580
370852
  async stop() {
370581
- return killProcess(electric);
370853
+ return killProcess(electric, { detached: true });
370582
370854
  }
370583
370855
  };
370584
370856
  }
@@ -370595,7 +370867,7 @@ async function waitForTcpPort2(host, port, timeoutMs = 3e4) {
370595
370867
  }
370596
370868
  function checkTcpPort2(host, port) {
370597
370869
  return new Promise((resolve52) => {
370598
- const socket = new net2.Socket();
370870
+ const socket = new net3.Socket();
370599
370871
  socket.setTimeout(1e3);
370600
370872
  socket.on("connect", () => {
370601
370873
  socket.destroy();
@@ -370781,22 +371053,29 @@ async function startDrizzleGateway(postgresInstances, port, configDir, options2)
370781
371053
  "drizzle-gateway",
370782
371054
  `Binary: ${binary.executables["drizzle-gateway"]}`
370783
371055
  );
371056
+ if (await isPortInUse(host, port)) {
371057
+ await reclaimSpecificOrphanOnPort(port);
371058
+ await waitForPortFree(host, port, 1e3);
371059
+ }
370784
371060
  const drizzleGateway = spawn4(binary.executables["drizzle-gateway"], [], {
370785
371061
  stdio: ["ignore", "pipe", "pipe"],
371062
+ detached: true,
370786
371063
  env: {
370787
371064
  ...process.env,
370788
371065
  STORE_PATH: drizzleConfigDir,
370789
371066
  PORT: String(port)
370790
371067
  }
370791
371068
  });
371069
+ drizzleGateway.unref();
370792
371070
  pipeProcess("drizzle-gateway", drizzleGateway);
370793
371071
  await waitForTcpPort3(host, port);
370794
371072
  return {
370795
371073
  port,
370796
371074
  url: `http://${host}:${port}`,
370797
371075
  pid: drizzleGateway.pid,
371076
+ detached: true,
370798
371077
  async stop() {
370799
- return killProcess(drizzleGateway);
371078
+ return killProcess(drizzleGateway, { detached: true });
370800
371079
  }
370801
371080
  };
370802
371081
  }
@@ -370815,7 +371094,7 @@ async function waitForTcpPort3(host, port, timeoutMs = 3e4) {
370815
371094
  }
370816
371095
  function checkTcpPort3(host, port) {
370817
371096
  return new Promise((resolve52) => {
370818
- const socket = new net3.Socket();
371097
+ const socket = new net4.Socket();
370819
371098
  socket.setTimeout(1e3);
370820
371099
  socket.on("connect", () => {
370821
371100
  socket.destroy();
@@ -371149,6 +371428,12 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371149
371428
  }
371150
371429
  const host = "127.0.0.1";
371151
371430
  const namespaceArgs = temporals.flatMap((t) => ["--namespace", t.name]);
371431
+ for (const p of [grpcPort, uiPort]) {
371432
+ if (await isPortInUse(host, p)) {
371433
+ await reclaimSpecificOrphanOnPort(p);
371434
+ await waitForPortFree(host, p, 1e3);
371435
+ }
371436
+ }
371152
371437
  const proc = spawn5(
371153
371438
  binary.executables["temporal"],
371154
371439
  [
@@ -371167,12 +371452,14 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371167
371452
  "pretty"
371168
371453
  ],
371169
371454
  {
371170
- stdio: ["ignore", "pipe", "pipe"]
371455
+ stdio: ["ignore", "pipe", "pipe"],
371456
+ detached: true
371171
371457
  }
371172
371458
  );
371459
+ proc.unref();
371173
371460
  pipeProcess("temporal", proc);
371174
371461
  await waitForTcpPort4(host, grpcPort);
371175
- const stopServer = () => killProcess(proc);
371462
+ const stopServer = () => killProcess(proc, { detached: true });
371176
371463
  const instances = temporals.map((temporal, i) => ({
371177
371464
  name: temporal.name,
371178
371465
  type: "temporal",
@@ -371184,6 +371471,7 @@ async function startTemporalDevServer(temporals, grpcPort, uiPort, dataDir, onPr
371184
371471
  url: `${host}:${grpcPort}`,
371185
371472
  uiPort,
371186
371473
  pid: i === 0 ? proc.pid : void 0,
371474
+ detached: i === 0 ? true : void 0,
371187
371475
  // Only the first instance owns the server lifecycle
371188
371476
  stop: i === 0 ? stopServer : async () => {
371189
371477
  }
@@ -371203,7 +371491,7 @@ async function waitForTcpPort4(host, port, timeoutMs = 3e4) {
371203
371491
  }
371204
371492
  function checkTcpPort4(host, port) {
371205
371493
  return new Promise((resolve52) => {
371206
- const socket = new net4.Socket();
371494
+ const socket = new net5.Socket();
371207
371495
  socket.setTimeout(1e3);
371208
371496
  socket.on("connect", () => {
371209
371497
  socket.destroy();
@@ -371305,6 +371593,8 @@ async function startResources(options2) {
371305
371593
  dbName: instance.dbName,
371306
371594
  url: instance.url
371307
371595
  };
371596
+ if (instance.pid !== void 0) dbState.pid = instance.pid;
371597
+ if (instance.detached) dbState.detached = true;
371308
371598
  if (instance.reshapeEnabled) {
371309
371599
  dbState.reshapeEnabled = true;
371310
371600
  if (instance.reshapeSearchPath) {
@@ -371327,7 +371617,7 @@ async function startResources(options2) {
371327
371617
  startedResources.push(instance);
371328
371618
  callbacks.onResourceReady?.(redis.name, instance);
371329
371619
  log(`Redis "${redis.name}" ready`);
371330
- await stateManager.registerDatabase(redis.name, {
371620
+ const redisState = {
371331
371621
  engine: "redis",
371332
371622
  port: instance.port,
371333
371623
  host: instance.host,
@@ -371335,7 +371625,10 @@ async function startResources(options2) {
371335
371625
  password: instance.password,
371336
371626
  dbName: instance.dbName,
371337
371627
  url: instance.url
371338
- });
371628
+ };
371629
+ if (instance.pid !== void 0) redisState.pid = instance.pid;
371630
+ if (instance.detached) redisState.detached = true;
371631
+ await stateManager.registerDatabase(redis.name, redisState);
371339
371632
  }
371340
371633
  for (const storage of storageConfigs) {
371341
371634
  if (signal?.cancelled) {
@@ -371389,6 +371682,8 @@ async function startResources(options2) {
371389
371682
  dbName: "",
371390
371683
  url: instance.url
371391
371684
  };
371685
+ if (instance.pid !== void 0) dbState.pid = instance.pid;
371686
+ if (instance.detached) dbState.detached = true;
371392
371687
  await stateManager.registerDatabase(instance.name, dbState);
371393
371688
  }
371394
371689
  }
@@ -371437,6 +371732,13 @@ async function startResources(options2) {
371437
371732
  syncUrl: electricInstance.url,
371438
371733
  syncSecret: electricInstance.secret
371439
371734
  });
371735
+ if (electricInstance.pid !== void 0) {
371736
+ await stateManager.registerElectric(pgName, {
371737
+ port: electricInstance.port,
371738
+ pid: electricInstance.pid,
371739
+ detached: electricInstance.detached
371740
+ });
371741
+ }
371440
371742
  callbacks.onElectricReady?.(pgName, electricInstance);
371441
371743
  log(`Electric sync for "${pgName}" ready at ${electricInstance.url}`);
371442
371744
  }
@@ -371522,7 +371824,7 @@ var BlockPortAllocator = class _BlockPortAllocator {
371522
371824
  };
371523
371825
  function isPortAvailable(port) {
371524
371826
  return new Promise((resolve52) => {
371525
- const server = net5.createServer();
371827
+ const server = net6.createServer();
371526
371828
  server.once("error", () => resolve52(false));
371527
371829
  server.once("listening", () => {
371528
371830
  server.close(() => resolve52(true));
@@ -371636,7 +371938,7 @@ var TunnelClientImpl = class extends EventEmitter2 {
371636
371938
  }
371637
371939
  }
371638
371940
  addPoolConnection() {
371639
- const remote = net6.connect({ host: this.tunnelHost, port: this.info.port });
371941
+ const remote = net7.connect({ host: this.tunnelHost, port: this.info.port });
371640
371942
  remote.setKeepAlive(true, 3e4);
371641
371943
  this.pool.add(remote);
371642
371944
  remote.once("data", (firstChunk) => {
@@ -371651,7 +371953,7 @@ var TunnelClientImpl = class extends EventEmitter2 {
371651
371953
  remote.on("close", () => this.onIdleClose(remote, errored));
371652
371954
  }
371653
371955
  pipeToLocal(remote, firstChunk) {
371654
- const local = net6.connect({ host: "127.0.0.1", port: this.localPort }, () => {
371956
+ const local = net7.connect({ host: "127.0.0.1", port: this.localPort }, () => {
371655
371957
  local.write(firstChunk);
371656
371958
  remote.pipe(local);
371657
371959
  local.pipe(remote);
@@ -371905,21 +372207,33 @@ var DevEnvironment = class extends TypedEventEmitter {
371905
372207
  for (const electric of this.electricInstances) {
371906
372208
  if (electric.pid) {
371907
372209
  try {
371908
- process.kill(electric.pid, "SIGKILL");
372210
+ if (electric.detached) {
372211
+ process.kill(-electric.pid, "SIGKILL");
372212
+ } else {
372213
+ process.kill(electric.pid, "SIGKILL");
372214
+ }
371909
372215
  } catch {
371910
372216
  }
371911
372217
  }
371912
372218
  }
371913
372219
  if (this.drizzleGateway?.pid) {
371914
372220
  try {
371915
- process.kill(this.drizzleGateway.pid, "SIGKILL");
372221
+ if (this.drizzleGateway.detached) {
372222
+ process.kill(-this.drizzleGateway.pid, "SIGKILL");
372223
+ } else {
372224
+ process.kill(this.drizzleGateway.pid, "SIGKILL");
372225
+ }
371916
372226
  } catch {
371917
372227
  }
371918
372228
  }
371919
372229
  for (const resource of this.resources.values()) {
371920
372230
  if (resource.pid) {
371921
372231
  try {
371922
- process.kill(resource.pid, "SIGKILL");
372232
+ if (resource.detached) {
372233
+ process.kill(-resource.pid, "SIGKILL");
372234
+ } else {
372235
+ process.kill(resource.pid, "SIGKILL");
372236
+ }
371923
372237
  } catch {
371924
372238
  }
371925
372239
  }
@@ -372073,21 +372387,33 @@ var DevEnvironment = class extends TypedEventEmitter {
372073
372387
  for (const electric of this.electricInstances) {
372074
372388
  if (electric.pid) {
372075
372389
  try {
372076
- process.kill(electric.pid, "SIGKILL");
372390
+ if (electric.detached) {
372391
+ process.kill(-electric.pid, "SIGKILL");
372392
+ } else {
372393
+ process.kill(electric.pid, "SIGKILL");
372394
+ }
372077
372395
  } catch {
372078
372396
  }
372079
372397
  }
372080
372398
  }
372081
372399
  if (this.drizzleGateway?.pid) {
372082
372400
  try {
372083
- process.kill(this.drizzleGateway.pid, "SIGKILL");
372401
+ if (this.drizzleGateway.detached) {
372402
+ process.kill(-this.drizzleGateway.pid, "SIGKILL");
372403
+ } else {
372404
+ process.kill(this.drizzleGateway.pid, "SIGKILL");
372405
+ }
372084
372406
  } catch {
372085
372407
  }
372086
372408
  }
372087
372409
  for (const resource of this.resources.values()) {
372088
372410
  if (resource.pid) {
372089
372411
  try {
372090
- process.kill(resource.pid, "SIGKILL");
372412
+ if (resource.detached) {
372413
+ process.kill(-resource.pid, "SIGKILL");
372414
+ } else {
372415
+ process.kill(resource.pid, "SIGKILL");
372416
+ }
372091
372417
  } catch {
372092
372418
  }
372093
372419
  }
@@ -372301,6 +372627,13 @@ var DevEnvironment = class extends TypedEventEmitter {
372301
372627
  );
372302
372628
  this.drizzleGateway = drizzleGateway;
372303
372629
  this.systemLog(`Database viewer ready at ${drizzleGateway.url}`);
372630
+ if (drizzleGateway.pid !== void 0) {
372631
+ await stateManager.registerDrizzleGateway({
372632
+ port: drizzleGateway.port,
372633
+ pid: drizzleGateway.pid,
372634
+ detached: drizzleGateway.detached
372635
+ });
372636
+ }
372304
372637
  } catch (err) {
372305
372638
  this.systemLog(
372306
372639
  `Failed to start database viewer: ${err instanceof Error ? err.message : String(err)}`
@@ -372524,6 +372857,7 @@ Add them to the config block in specific.local`
372524
372857
  publicUrls.set(svc.name, `localhost:${svc.port}`);
372525
372858
  }
372526
372859
  }
372860
+ await stateManager.setPublicUrls(Object.fromEntries(publicUrls));
372527
372861
  const runningServices = [];
372528
372862
  const resolveServiceCwd = (service) => {
372529
372863
  if (service.root)
@@ -373341,7 +373675,7 @@ function trackEvent(event, properties) {
373341
373675
  event,
373342
373676
  properties: {
373343
373677
  ...properties,
373344
- cli_version: "0.1.125",
373678
+ cli_version: "0.1.127",
373345
373679
  platform: process.platform,
373346
373680
  node_version: process.version,
373347
373681
  project_id: getProjectId()
@@ -373659,7 +373993,7 @@ Valid agents: ${VALID_AGENT_IDS.join(", ")}`
373659
373993
  }
373660
373994
 
373661
373995
  // src/commands/docs.tsx
373662
- import { readFileSync as readFileSync9, existsSync as existsSync17 } from "fs";
373996
+ import { readFileSync as readFileSync10, existsSync as existsSync17 } from "fs";
373663
373997
  import { spawn as spawn6 } from "child_process";
373664
373998
  import { join as join19, dirname as dirname8 } from "path";
373665
373999
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -373673,7 +374007,7 @@ var BETA_REGISTRY = [
373673
374007
  ];
373674
374008
 
373675
374009
  // src/lib/beta/storage.ts
373676
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
374010
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync16, mkdirSync as mkdirSync13 } from "fs";
373677
374011
  import { join as join18 } from "path";
373678
374012
  var BETAS_FILE = ".specific/betas.json";
373679
374013
  function loadEnabledBetas(projectDir = process.cwd()) {
@@ -373682,7 +374016,7 @@ function loadEnabledBetas(projectDir = process.cwd()) {
373682
374016
  return [];
373683
374017
  }
373684
374018
  try {
373685
- const content = readFileSync8(filePath, "utf-8");
374019
+ const content = readFileSync9(filePath, "utf-8");
373686
374020
  const data = JSON.parse(content);
373687
374021
  return data.enabled ?? [];
373688
374022
  } catch {
@@ -373787,15 +374121,15 @@ function resolveEmbeddedDoc(path27) {
373787
374121
  function resolveFilesystemDoc(path27) {
373788
374122
  if (!path27) {
373789
374123
  const indexPath2 = join19(docsDir, "index.md");
373790
- return existsSync17(indexPath2) ? readFileSync9(indexPath2, "utf-8") : null;
374124
+ return existsSync17(indexPath2) ? readFileSync10(indexPath2, "utf-8") : null;
373791
374125
  }
373792
374126
  const directPath = join19(docsDir, `${path27}.md`);
373793
374127
  if (existsSync17(directPath)) {
373794
- return readFileSync9(directPath, "utf-8");
374128
+ return readFileSync10(directPath, "utf-8");
373795
374129
  }
373796
374130
  const indexPath = join19(docsDir, path27, "index.md");
373797
374131
  if (existsSync17(indexPath)) {
373798
- return readFileSync9(indexPath, "utf-8");
374132
+ return readFileSync10(indexPath, "utf-8");
373799
374133
  }
373800
374134
  return null;
373801
374135
  }
@@ -374879,11 +375213,13 @@ function DevUI({ instanceKey, tunnelEnabled }) {
374879
375213
  };
374880
375214
  process.on("SIGINT", handleSignal);
374881
375215
  process.on("SIGTERM", handleSignal);
375216
+ process.on("SIGHUP", handleSignal);
374882
375217
  process.on("uncaughtException", handleCrash);
374883
375218
  process.on("unhandledRejection", handleCrash);
374884
375219
  return () => {
374885
375220
  process.off("SIGINT", handleSignal);
374886
375221
  process.off("SIGTERM", handleSignal);
375222
+ process.off("SIGHUP", handleSignal);
374887
375223
  process.off("uncaughtException", handleCrash);
374888
375224
  process.off("unhandledRejection", handleCrash);
374889
375225
  };
@@ -376585,7 +376921,7 @@ function startSpinner(text) {
376585
376921
  }
376586
376922
  };
376587
376923
  }
376588
- async function execCommand(serviceName, command, instanceKey = "default") {
376924
+ async function execCommand(serviceName, command, instanceKey = "default", options2 = {}) {
376589
376925
  if (command.length === 0) {
376590
376926
  console.error(
376591
376927
  "Error: No command provided. Usage: specific exec <service> -- <command>"
@@ -376723,11 +377059,32 @@ async function execCommand(serviceName, command, instanceKey = "default") {
376723
377059
  }
376724
377060
  }
376725
377061
  }
377062
+ let runningContext;
377063
+ if (existingInstances) {
377064
+ const serviceEndpoints = /* @__PURE__ */ new Map();
377065
+ for (const [name, svcState] of Object.entries(existingInstances.services)) {
377066
+ if (svcState.port !== void 0) {
377067
+ serviceEndpoints.set(name, [
377068
+ { serviceName: name, endpointName: "default", port: svcState.port }
377069
+ ]);
377070
+ }
377071
+ }
377072
+ const selfPort = existingInstances.services[serviceName]?.port;
377073
+ runningContext = { serviceEndpoints };
377074
+ if (selfPort !== void 0) {
377075
+ runningContext.servicePort = selfPort;
377076
+ runningContext.currentServicePorts = /* @__PURE__ */ new Map([["default", selfPort]]);
377077
+ }
377078
+ if (existingInstances.publicUrls) {
377079
+ runningContext.publicUrls = new Map(Object.entries(existingInstances.publicUrls));
377080
+ }
377081
+ }
377082
+ const effectiveEnv = runningContext && service.dev?.env ? { ...service.env, ...service.dev.env } : service.env;
376726
377083
  let resolvedEnv;
376727
377084
  try {
376728
377085
  const secrets = await prepareSecrets(config.secrets);
376729
377086
  const configs = await prepareConfigs(config.configs);
376730
- const result = resolveEnvForExec(service.env, resources, secrets, configs);
377087
+ const result = resolveEnvForExec(effectiveEnv, resources, secrets, configs, runningContext);
376731
377088
  resolvedEnv = result.env;
376732
377089
  if (result.missingSecrets.length > 0 || result.missingConfigs.length > 0) {
376733
377090
  if (result.missingSecrets.length > 0) {
@@ -376762,7 +377119,9 @@ async function execCommand(serviceName, command, instanceKey = "default") {
376762
377119
  process.on("SIGINT", () => handleSignal("SIGINT"));
376763
377120
  process.on("SIGTERM", () => handleSignal("SIGTERM"));
376764
377121
  let effectiveCwd = process.cwd();
376765
- if (service.root) {
377122
+ if (options2.cwd) {
377123
+ effectiveCwd = path21.resolve(process.cwd(), options2.cwd);
377124
+ } else if (service.root) {
376766
377125
  effectiveCwd = path21.resolve(process.cwd(), service.root);
376767
377126
  } else if (service.build) {
376768
377127
  const build = config.builds.find((b) => b.name === service.build.name);
@@ -377422,7 +377781,7 @@ function compareVersions(a, b) {
377422
377781
  return 0;
377423
377782
  }
377424
377783
  async function checkForUpdate() {
377425
- const currentVersion = "0.1.125";
377784
+ const currentVersion = "0.1.127";
377426
377785
  const response = await fetch(`${BINARIES_BASE_URL}/latest?t=${Date.now()}`);
377427
377786
  if (!response.ok) {
377428
377787
  throw new Error(`Failed to check for updates: HTTP ${response.status}`);
@@ -377692,7 +378051,7 @@ async function projectListCommand() {
377692
378051
  var program = new Command();
377693
378052
  var env = "production";
377694
378053
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
377695
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.125").enablePositionalOptions();
378054
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.127").enablePositionalOptions();
377696
378055
  program.command("init").description("Initialize project for use with a coding agent").option("--agent <name...>", "Agents to configure (cursor, claude, codex, other)").addHelpText("after", `
377697
378056
  Examples:
377698
378057
  $ specific init
@@ -377722,13 +378081,14 @@ Examples:
377722
378081
  $ specific deploy --secret db_url=postgres://... --config domain=app.com`).action((options2) => {
377723
378082
  deployCommand(options2);
377724
378083
  });
377725
- program.command("exec <service> [args...]").description("Run a one-off command with service environment").option("-k, --key <key>", "Dev environment namespace (auto-detected from git worktree if not specified)").passThroughOptions().addHelpText("after", `
378084
+ program.command("exec <service> [args...]").description("Run a one-off command with service environment").option("-k, --key <key>", "Dev environment namespace (auto-detected from git worktree if not specified)").option("--cwd <path>", "Override working directory (defaults to service.root or build.root)").passThroughOptions().addHelpText("after", `
377726
378085
  Examples:
377727
378086
  $ specific exec api -- npm run migrate
377728
- $ specific exec worker -- python manage.py shell`).action(async (service, args, options2) => {
378087
+ $ specific exec worker -- python manage.py shell
378088
+ $ specific exec --cwd . api -- npx expo start`).action(async (service, args, options2) => {
377729
378089
  const filteredArgs = args[0] === "--" ? args.slice(1) : args;
377730
378090
  const key = options2.key ?? getDefaultKey();
377731
- await execCommand(service, filteredArgs, key);
378091
+ await execCommand(service, filteredArgs, key, options2.cwd !== void 0 ? { cwd: options2.cwd } : {});
377732
378092
  });
377733
378093
  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().addHelpText("after", `
377734
378094
  Examples: