@specific.dev/cli 0.1.50 → 0.1.52

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 (33) 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 +226 -114
  27. package/dist/docs/builds.md +42 -0
  28. package/dist/docs/services.md +53 -0
  29. package/dist/postinstall.js +1 -1
  30. package/package.json +2 -2
  31. /package/dist/admin/_next/static/{o2Qo92jA0gWbtB1ZWKQFF → 0CmgQNPZi3W-iwSoebP4u}/_buildManifest.js +0 -0
  32. /package/dist/admin/_next/static/{o2Qo92jA0gWbtB1ZWKQFF → 0CmgQNPZi3W-iwSoebP4u}/_clientMiddlewareManifest.json +0 -0
  33. /package/dist/admin/_next/static/{o2Qo92jA0gWbtB1ZWKQFF → 0CmgQNPZi3W-iwSoebP4u}/_ssgManifest.js +0 -0
package/dist/cli.js CHANGED
@@ -637,19 +637,19 @@ var init_open = __esm({
637
637
  }
638
638
  const subprocess = childProcess3.spawn(command, cliArguments, childProcessOptions);
639
639
  if (options2.wait) {
640
- return new Promise((resolve9, reject) => {
640
+ return new Promise((resolve10, reject) => {
641
641
  subprocess.once("error", reject);
642
642
  subprocess.once("close", (exitCode) => {
643
643
  if (!options2.allowNonzeroExitCode && exitCode !== 0) {
644
644
  reject(new Error(`Exited with code ${exitCode}`));
645
645
  return;
646
646
  }
647
- resolve9(subprocess);
647
+ resolve10(subprocess);
648
648
  });
649
649
  });
650
650
  }
651
651
  if (isFallbackAttempt) {
652
- return new Promise((resolve9, reject) => {
652
+ return new Promise((resolve10, reject) => {
653
653
  subprocess.once("error", reject);
654
654
  subprocess.once("spawn", () => {
655
655
  subprocess.once("close", (exitCode) => {
@@ -659,17 +659,17 @@ var init_open = __esm({
659
659
  return;
660
660
  }
661
661
  subprocess.unref();
662
- resolve9(subprocess);
662
+ resolve10(subprocess);
663
663
  });
664
664
  });
665
665
  });
666
666
  }
667
667
  subprocess.unref();
668
- return new Promise((resolve9, reject) => {
668
+ return new Promise((resolve10, reject) => {
669
669
  subprocess.once("error", reject);
670
670
  subprocess.once("spawn", () => {
671
671
  subprocess.off("error", reject);
672
- resolve9(subprocess);
672
+ resolve10(subprocess);
673
673
  });
674
674
  });
675
675
  };
@@ -183682,7 +183682,7 @@ async function pollUntilToken(deviceAuth, isCancelled) {
183682
183682
  return null;
183683
183683
  }
183684
183684
  function sleep(ms) {
183685
- return new Promise((resolve9) => setTimeout(resolve9, ms));
183685
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
183686
183686
  }
183687
183687
 
183688
183688
  // src/lib/auth/login.tsx
@@ -183699,7 +183699,7 @@ function LoginUI({
183699
183699
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", gap: 1 }, isReauthentication && /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "Session expired. Please log in again."), /* @__PURE__ */ React.createElement(Text, { bold: true }, "Log in to Specific"), state.phase === "waiting-for-browser" ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column" }, /* @__PURE__ */ React.createElement(Text, null, "Your authentication code:", " ", /* @__PURE__ */ React.createElement(Text, { color: "cyan", bold: true }, state.userCode))), /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Waiting for authentication in browser...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "If the browser didn't open, visit: ", state.verificationUri)) : /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { color: "blue" }, /* @__PURE__ */ React.createElement(Spinner, { type: "dots" })), /* @__PURE__ */ React.createElement(Text, null, " Initiating login...")));
183700
183700
  }
183701
183701
  function performLogin(options2 = {}) {
183702
- return new Promise((resolve9) => {
183702
+ return new Promise((resolve10) => {
183703
183703
  let currentState = { phase: "initiating" };
183704
183704
  let flowHandle;
183705
183705
  const instance = render(
@@ -183739,14 +183739,14 @@ function performLogin(options2 = {}) {
183739
183739
  process.off("SIGINT", handleExit);
183740
183740
  process.off("SIGTERM", handleExit);
183741
183741
  instance.unmount();
183742
- resolve9({ success: true, userEmail: newState.email });
183742
+ resolve10({ success: true, userEmail: newState.email });
183743
183743
  }, 100);
183744
183744
  } else if (newState.phase === "error") {
183745
183745
  setTimeout(() => {
183746
183746
  process.off("SIGINT", handleExit);
183747
183747
  process.off("SIGTERM", handleExit);
183748
183748
  instance.unmount();
183749
- resolve9({ success: false, error: new Error(newState.message) });
183749
+ resolve10({ success: false, error: new Error(newState.message) });
183750
183750
  }, 100);
183751
183751
  }
183752
183752
  }
@@ -183873,7 +183873,7 @@ function trackEvent(event, properties) {
183873
183873
  event,
183874
183874
  properties: {
183875
183875
  ...properties,
183876
- cli_version: "0.1.50",
183876
+ cli_version: "0.1.52",
183877
183877
  platform: process.platform,
183878
183878
  node_version: process.version,
183879
183879
  project_id: getProjectId(),
@@ -184348,8 +184348,43 @@ function parseReference(value) {
184348
184348
  if (match && match[1]) {
184349
184349
  return parseReferenceString(match[1]);
184350
184350
  }
184351
+ if (value.includes("${")) {
184352
+ return parseInterpolatedString(value);
184353
+ }
184351
184354
  return value;
184352
184355
  }
184356
+ function parseInterpolatedString(value) {
184357
+ const parts = [];
184358
+ const regex = /\$\{([^}]+)\}/g;
184359
+ let lastIndex = 0;
184360
+ let match;
184361
+ while ((match = regex.exec(value)) !== null) {
184362
+ if (match.index > lastIndex) {
184363
+ parts.push({ type: "literal", value: value.slice(lastIndex, match.index) });
184364
+ }
184365
+ const refStr = match[1];
184366
+ const parsed = parseReferenceString(refStr);
184367
+ if (typeof parsed === "string") {
184368
+ parts.push({ type: "literal", value: match[0] });
184369
+ } else {
184370
+ if (parsed.type === "secret") {
184371
+ throw new Error(`Secret references cannot be used inside interpolated strings. Use \${secret.${parsed.name}} as a standalone env var value instead.`);
184372
+ }
184373
+ if (parsed.type === "config") {
184374
+ throw new Error(`Config references cannot be used inside interpolated strings. Use \${config.${parsed.name}} as a standalone env var value instead.`);
184375
+ }
184376
+ if (parsed.type === "build") {
184377
+ throw new Error(`Build references cannot be used inside interpolated strings.`);
184378
+ }
184379
+ parts.push({ type: "ref", ref: parsed });
184380
+ }
184381
+ lastIndex = match.index + match[0].length;
184382
+ }
184383
+ if (lastIndex < value.length) {
184384
+ parts.push({ type: "literal", value: value.slice(lastIndex) });
184385
+ }
184386
+ return { type: "interpolated", parts };
184387
+ }
184353
184388
  function parseReferenceString(str) {
184354
184389
  if (str === "port") {
184355
184390
  return { type: "port" };
@@ -184529,6 +184564,9 @@ function parseBuilds(buildData) {
184529
184564
  if (fieldObj.context) {
184530
184565
  build.context = String(fieldObj.context);
184531
184566
  }
184567
+ if (fieldObj.root) {
184568
+ build.root = String(fieldObj.root);
184569
+ }
184532
184570
  if (dev) {
184533
184571
  build.dev = dev;
184534
184572
  }
@@ -184539,6 +184577,16 @@ function parseBuilds(buildData) {
184539
184577
  continue;
184540
184578
  if (value.type === "service" && value.attribute === "public_url")
184541
184579
  continue;
184580
+ if (value.type === "interpolated") {
184581
+ for (const part of value.parts) {
184582
+ if (part.type === "ref") {
184583
+ if (part.ref.type !== "service" || part.ref.attribute !== "public_url") {
184584
+ throw new Error(`Build "${name}" env var "${key}" uses an unsupported reference type in interpolated string. Build env vars only support string literals and \${service.<name>.public_url} references.`);
184585
+ }
184586
+ }
184587
+ }
184588
+ continue;
184589
+ }
184542
184590
  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.`);
184543
184591
  }
184544
184592
  build.env = env2;
@@ -184616,6 +184664,9 @@ function parseServices(serviceData) {
184616
184664
  if (fieldObj.command) {
184617
184665
  service.command = String(fieldObj.command);
184618
184666
  }
184667
+ if (fieldObj.root) {
184668
+ service.root = String(fieldObj.root);
184669
+ }
184619
184670
  const endpoints = parseEndpoints(fieldObj);
184620
184671
  if (fieldObj.expose !== void 0) {
184621
184672
  service.expose = {};
@@ -184826,12 +184877,21 @@ async function parseConfig(hcl) {
184826
184877
  environments: parseEnvironments(json.environment)
184827
184878
  };
184828
184879
  }
184880
+ function envValueReferences(value) {
184881
+ if (typeof value === "string")
184882
+ return [];
184883
+ if (value.type === "interpolated") {
184884
+ return value.parts.filter((p) => p.type === "ref").map((p) => p.ref);
184885
+ }
184886
+ return [value];
184887
+ }
184829
184888
  function hasPortReference(env2) {
184830
184889
  if (!env2)
184831
184890
  return false;
184832
184891
  for (const value of Object.values(env2)) {
184833
- if (typeof value === "object" && value.type === "port") {
184834
- return true;
184892
+ for (const ref of envValueReferences(value)) {
184893
+ if (ref.type === "port")
184894
+ return true;
184835
184895
  }
184836
184896
  }
184837
184897
  return false;
@@ -184843,20 +184903,19 @@ function validateEndpointReferences(config) {
184843
184903
  serviceEndpointsMap.set(service.name, service.endpoints);
184844
184904
  }
184845
184905
  for (const service of config.services) {
184846
- if (service.endpoints.length > 1 && service.env) {
184847
- for (const [key, value] of Object.entries(service.env)) {
184848
- if (typeof value === "object" && value.type === "port") {
184906
+ if (!service.env)
184907
+ continue;
184908
+ for (const [key, value] of Object.entries(service.env)) {
184909
+ const refs = envValueReferences(value);
184910
+ for (const ref of refs) {
184911
+ if (service.endpoints.length > 1 && ref.type === "port") {
184849
184912
  errors.push({
184850
184913
  service: service.name,
184851
184914
  message: `Service "${service.name}" has multiple endpoints but uses \${port} for env var "${key}". Use \${endpoint.<name>.port} instead.`
184852
184915
  });
184853
184916
  }
184854
- }
184855
- }
184856
- if (service.env) {
184857
- for (const [key, value] of Object.entries(service.env)) {
184858
- if (typeof value === "object" && value.type === "service") {
184859
- const serviceRef = value;
184917
+ if (ref.type === "service") {
184918
+ const serviceRef = ref;
184860
184919
  const targetEndpoints = serviceEndpointsMap.get(serviceRef.serviceName);
184861
184920
  if (!targetEndpoints) {
184862
184921
  errors.push({
@@ -184901,8 +184960,8 @@ function validateEndpointReferences(config) {
184901
184960
  }
184902
184961
  }
184903
184962
  }
184904
- if (typeof value === "object" && value.type === "endpoint") {
184905
- const endpointRef = value;
184963
+ if (ref.type === "endpoint") {
184964
+ const endpointRef = ref;
184906
184965
  const endpointExists = service.endpoints.some((e) => e.name === endpointRef.endpointName);
184907
184966
  if (!endpointExists) {
184908
184967
  errors.push({
@@ -185055,10 +185114,10 @@ async function downloadFile(url, destPath, onProgress) {
185055
185114
  });
185056
185115
  }
185057
185116
  }
185058
- await new Promise((resolve9, reject) => {
185117
+ await new Promise((resolve10, reject) => {
185059
185118
  fileStream.end((err) => {
185060
185119
  if (err) reject(err);
185061
- else resolve9();
185120
+ else resolve10();
185062
185121
  });
185063
185122
  });
185064
185123
  fs11.renameSync(partPath, destPath);
@@ -185241,7 +185300,7 @@ var drizzleGatewayBinary = {
185241
185300
  // src/lib/bin/definitions/reshape.ts
185242
185301
  var reshapeBinary = {
185243
185302
  name: "reshape",
185244
- versions: ["0.9.0"],
185303
+ versions: ["0.9.1"],
185245
185304
  // GitHub release assets
185246
185305
  urlTemplate: "https://github.com/fabianlindfors/reshape/releases/download/v{version}/reshape-{arch}",
185247
185306
  platformMapping: {
@@ -185265,13 +185324,13 @@ async function runReshapeCheck(migrationsDir) {
185265
185324
  try {
185266
185325
  const binary = await ensureBinary(reshapeBinary);
185267
185326
  const reshapePath = binary.executables["reshape"];
185268
- return new Promise((resolve9) => {
185327
+ return new Promise((resolve10) => {
185269
185328
  execFile7(reshapePath, ["check", "--dirs", migrationsDir], (err, _stdout, stderr) => {
185270
185329
  if (err) {
185271
185330
  const errorMsg = stderr.trim() || err.message;
185272
- resolve9({ success: false, error: errorMsg });
185331
+ resolve10({ success: false, error: errorMsg });
185273
185332
  } else {
185274
- resolve9({ success: true });
185333
+ resolve10({ success: true });
185275
185334
  }
185276
185335
  });
185277
185336
  });
@@ -186228,7 +186287,7 @@ var NodeFsHandler = class {
186228
186287
  this._addToNodeFs(path26, initialAdd, wh, depth + 1);
186229
186288
  }
186230
186289
  }).on(EV.ERROR, this._boundHandleError);
186231
- return new Promise((resolve9, reject) => {
186290
+ return new Promise((resolve10, reject) => {
186232
186291
  if (!stream)
186233
186292
  return reject();
186234
186293
  stream.once(STR_END, () => {
@@ -186237,7 +186296,7 @@ var NodeFsHandler = class {
186237
186296
  return;
186238
186297
  }
186239
186298
  const wasThrottled = throttler ? throttler.clear() : false;
186240
- resolve9(void 0);
186299
+ resolve10(void 0);
186241
186300
  previous.getChildren().filter((item) => {
186242
186301
  return item !== directory && !current.has(item);
186243
186302
  }).forEach((item) => {
@@ -187301,7 +187360,7 @@ async function startStorage(storage, port, dataDir) {
187301
187360
  };
187302
187361
  }
187303
187362
  async function runCommand(command, args, env2) {
187304
- return new Promise((resolve9, reject) => {
187363
+ return new Promise((resolve10, reject) => {
187305
187364
  const proc = spawn(command, args, {
187306
187365
  stdio: ["ignore", "pipe", "pipe"],
187307
187366
  env: env2
@@ -187312,7 +187371,7 @@ async function runCommand(command, args, env2) {
187312
187371
  });
187313
187372
  proc.on("close", (code) => {
187314
187373
  if (code === 0) {
187315
- resolve9();
187374
+ resolve10();
187316
187375
  } else {
187317
187376
  reject(new Error(`Command failed with code ${code}: ${stderr}`));
187318
187377
  }
@@ -187321,7 +187380,7 @@ async function runCommand(command, args, env2) {
187321
187380
  });
187322
187381
  }
187323
187382
  async function createPostgresDatabase(postgresPath, dataDir, dbName, env2) {
187324
- return new Promise((resolve9, reject) => {
187383
+ return new Promise((resolve10, reject) => {
187325
187384
  const proc = spawn(
187326
187385
  postgresPath,
187327
187386
  ["--single", "-D", dataDir, "postgres"],
@@ -187335,7 +187394,7 @@ async function createPostgresDatabase(postgresPath, dataDir, dbName, env2) {
187335
187394
  stderr += data.toString();
187336
187395
  });
187337
187396
  proc.on("close", (code) => {
187338
- resolve9();
187397
+ resolve10();
187339
187398
  });
187340
187399
  proc.on("error", reject);
187341
187400
  proc.stdin?.write(`CREATE DATABASE "${dbName}";
@@ -187355,33 +187414,33 @@ async function waitForTcpPort(host, port, timeoutMs = 3e4) {
187355
187414
  throw new Error(`Port ${port} did not become available within timeout`);
187356
187415
  }
187357
187416
  function checkTcpPort(host, port) {
187358
- return new Promise((resolve9) => {
187417
+ return new Promise((resolve10) => {
187359
187418
  const socket = new net.Socket();
187360
187419
  socket.setTimeout(1e3);
187361
187420
  socket.on("connect", () => {
187362
187421
  socket.destroy();
187363
- resolve9(true);
187422
+ resolve10(true);
187364
187423
  });
187365
187424
  socket.on("timeout", () => {
187366
187425
  socket.destroy();
187367
- resolve9(false);
187426
+ resolve10(false);
187368
187427
  });
187369
187428
  socket.on("error", () => {
187370
187429
  socket.destroy();
187371
- resolve9(false);
187430
+ resolve10(false);
187372
187431
  });
187373
187432
  socket.connect(port, host);
187374
187433
  });
187375
187434
  }
187376
187435
  async function stopProcess(proc) {
187377
- return new Promise((resolve9) => {
187436
+ return new Promise((resolve10) => {
187378
187437
  if (proc.killed || proc.exitCode !== null) {
187379
- resolve9();
187438
+ resolve10();
187380
187439
  return;
187381
187440
  }
187382
187441
  proc.once("exit", () => {
187383
187442
  clearTimeout(forceKillTimeout);
187384
- resolve9();
187443
+ resolve10();
187385
187444
  });
187386
187445
  proc.kill("SIGTERM");
187387
187446
  const forceKillTimeout = setTimeout(() => {
@@ -187392,7 +187451,7 @@ async function stopProcess(proc) {
187392
187451
  });
187393
187452
  }
187394
187453
  function sleep2(ms) {
187395
- return new Promise((resolve9) => setTimeout(resolve9, ms));
187454
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
187396
187455
  }
187397
187456
 
187398
187457
  // src/lib/dev/service-runner.ts
@@ -187545,6 +187604,21 @@ function resolveEnvValue(value, resources, secrets, configs, servicePort, servic
187545
187604
  if (typeof value === "string") {
187546
187605
  return value;
187547
187606
  }
187607
+ if (value.type === "interpolated") {
187608
+ return value.parts.map((part) => {
187609
+ if (part.type === "literal") return part.value;
187610
+ return resolveEnvValue(
187611
+ part.ref,
187612
+ resources,
187613
+ secrets,
187614
+ configs,
187615
+ servicePort,
187616
+ serviceEndpoints,
187617
+ currentServicePorts,
187618
+ publicUrls
187619
+ );
187620
+ }).join("");
187621
+ }
187548
187622
  switch (value.type) {
187549
187623
  case "port":
187550
187624
  if (servicePort === void 0) {
@@ -187740,13 +187814,23 @@ function resolveEnvForExec(env2, resources, secrets, configs) {
187740
187814
  if (value.type === "port" || value.type === "endpoint" || value.type === "service") {
187741
187815
  continue;
187742
187816
  }
187817
+ if (value.type === "interpolated") {
187818
+ const hasSkippableRef = value.parts.some(
187819
+ (part) => part.type === "ref" && (part.ref.type === "port" || part.ref.type === "endpoint" || part.ref.type === "service")
187820
+ );
187821
+ if (hasSkippableRef) {
187822
+ continue;
187823
+ }
187824
+ resolved[key] = resolveEnvValue(value, resources, secrets, configs);
187825
+ continue;
187826
+ }
187743
187827
  resolved[key] = resolveEnvValue(value, resources, secrets, configs);
187744
187828
  }
187745
187829
  return resolved;
187746
187830
  }
187747
187831
 
187748
187832
  // src/lib/dev/service-runner.ts
187749
- function startService(service, resources, secrets, configs, endpointPorts, serviceEndpoints, onLog, publicUrls) {
187833
+ function startService(service, resources, secrets, configs, endpointPorts, serviceEndpoints, onLog, publicUrls, cwd) {
187750
187834
  const command = service.dev?.command ?? service.command;
187751
187835
  if (!command) {
187752
187836
  throw new Error(`Service "${service.name}" has no command`);
@@ -187768,7 +187852,7 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
187768
187852
  );
187769
187853
  const child = spawn2(command, {
187770
187854
  shell: true,
187771
- cwd: process.cwd(),
187855
+ cwd: cwd || process.cwd(),
187772
187856
  env: {
187773
187857
  ...process.env,
187774
187858
  ...resolvedEnv
@@ -187796,14 +187880,14 @@ function startService(service, resources, secrets, configs, endpointPorts, servi
187796
187880
  ports: endpointPorts,
187797
187881
  process: child,
187798
187882
  async stop() {
187799
- return new Promise((resolve9) => {
187883
+ return new Promise((resolve10) => {
187800
187884
  if (child.killed || child.exitCode !== null) {
187801
- resolve9();
187885
+ resolve10();
187802
187886
  return;
187803
187887
  }
187804
187888
  child.once("exit", () => {
187805
187889
  clearTimeout(forceKillTimeout);
187806
- resolve9();
187890
+ resolve10();
187807
187891
  });
187808
187892
  child.kill("SIGTERM");
187809
187893
  const forceKillTimeout = setTimeout(() => {
@@ -187884,7 +187968,7 @@ var InstanceStateManager = class {
187884
187968
  }
187885
187969
  continue;
187886
187970
  }
187887
- await new Promise((resolve9) => setTimeout(resolve9, 100));
187971
+ await new Promise((resolve10) => setTimeout(resolve10, 100));
187888
187972
  } else {
187889
187973
  throw e;
187890
187974
  }
@@ -188167,7 +188251,7 @@ async function startHttpProxy(services, certificate, getState, instanceKey = "de
188167
188251
  handleRequest
188168
188252
  );
188169
188253
  httpsServer.on("upgrade", handleUpgrade);
188170
- return new Promise((resolve9, reject) => {
188254
+ return new Promise((resolve10, reject) => {
188171
188255
  let httpStarted = false;
188172
188256
  let httpsStarted = false;
188173
188257
  let failed = false;
@@ -188178,7 +188262,7 @@ async function startHttpProxy(services, certificate, getState, instanceKey = "de
188178
188262
  "proxy",
188179
188263
  `HTTP/HTTPS proxy started on ports ${HTTP_PORT}/${HTTPS_PORT}`
188180
188264
  );
188181
- resolve9({
188265
+ resolve10({
188182
188266
  httpPort: HTTP_PORT,
188183
188267
  httpsPort: HTTPS_PORT,
188184
188268
  updateServices,
@@ -188190,13 +188274,13 @@ async function startHttpProxy(services, certificate, getState, instanceKey = "de
188190
188274
  writeLog("proxy", "Certificate updated");
188191
188275
  },
188192
188276
  async stop() {
188193
- return new Promise((resolve10) => {
188277
+ return new Promise((resolve11) => {
188194
188278
  let closed = 0;
188195
188279
  const onClose = () => {
188196
188280
  closed++;
188197
188281
  if (closed === 2) {
188198
188282
  clearTimeout(forceCloseTimeout);
188199
- resolve10();
188283
+ resolve11();
188200
188284
  }
188201
188285
  };
188202
188286
  httpServer.close(onClose);
@@ -188204,7 +188288,7 @@ async function startHttpProxy(services, certificate, getState, instanceKey = "de
188204
188288
  const forceCloseTimeout = setTimeout(() => {
188205
188289
  httpServer.closeAllConnections?.();
188206
188290
  httpsServer.closeAllConnections?.();
188207
- resolve10();
188291
+ resolve11();
188208
188292
  }, 2e3);
188209
188293
  });
188210
188294
  }
@@ -188367,7 +188451,7 @@ function serveFileContent(res, filePath, contentType, statusCode = 200) {
188367
188451
  }
188368
188452
  }
188369
188453
  async function startAdminServer(getState) {
188370
- return new Promise((resolve9, reject) => {
188454
+ return new Promise((resolve10, reject) => {
188371
188455
  const server = http.createServer((req, res) => {
188372
188456
  const url = new URL(req.url || "/", "http://localhost");
188373
188457
  res.setHeader("Access-Control-Allow-Origin", "*");
@@ -188399,7 +188483,7 @@ async function startAdminServer(getState) {
188399
188483
  }
188400
188484
  const port = addr.port;
188401
188485
  writeLog("admin", `Admin API server started on port ${port}`);
188402
- resolve9({
188486
+ resolve10({
188403
188487
  port,
188404
188488
  stop: () => new Promise((res, rej) => {
188405
188489
  server.close((err) => err ? rej(err) : res());
@@ -188560,33 +188644,33 @@ async function waitForTcpPort2(host, port, timeoutMs = 3e4) {
188560
188644
  throw new Error(`Electric port ${port} did not become available within timeout`);
188561
188645
  }
188562
188646
  function checkTcpPort2(host, port) {
188563
- return new Promise((resolve9) => {
188647
+ return new Promise((resolve10) => {
188564
188648
  const socket = new net2.Socket();
188565
188649
  socket.setTimeout(1e3);
188566
188650
  socket.on("connect", () => {
188567
188651
  socket.destroy();
188568
- resolve9(true);
188652
+ resolve10(true);
188569
188653
  });
188570
188654
  socket.on("timeout", () => {
188571
188655
  socket.destroy();
188572
- resolve9(false);
188656
+ resolve10(false);
188573
188657
  });
188574
188658
  socket.on("error", () => {
188575
188659
  socket.destroy();
188576
- resolve9(false);
188660
+ resolve10(false);
188577
188661
  });
188578
188662
  socket.connect(port, host);
188579
188663
  });
188580
188664
  }
188581
188665
  async function stopProcess2(proc) {
188582
- return new Promise((resolve9) => {
188666
+ return new Promise((resolve10) => {
188583
188667
  if (proc.killed || proc.exitCode !== null) {
188584
- resolve9();
188668
+ resolve10();
188585
188669
  return;
188586
188670
  }
188587
188671
  proc.once("exit", () => {
188588
188672
  clearTimeout(forceKillTimeout);
188589
- resolve9();
188673
+ resolve10();
188590
188674
  });
188591
188675
  proc.kill("SIGTERM");
188592
188676
  const forceKillTimeout = setTimeout(() => {
@@ -188597,7 +188681,7 @@ async function stopProcess2(proc) {
188597
188681
  });
188598
188682
  }
188599
188683
  function sleep3(ms) {
188600
- return new Promise((resolve9) => setTimeout(resolve9, ms));
188684
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
188601
188685
  }
188602
188686
 
188603
188687
  // src/lib/dev/drizzle-gateway-manager.ts
@@ -188681,33 +188765,33 @@ async function waitForTcpPort3(host, port, timeoutMs = 3e4) {
188681
188765
  );
188682
188766
  }
188683
188767
  function checkTcpPort3(host, port) {
188684
- return new Promise((resolve9) => {
188768
+ return new Promise((resolve10) => {
188685
188769
  const socket = new net3.Socket();
188686
188770
  socket.setTimeout(1e3);
188687
188771
  socket.on("connect", () => {
188688
188772
  socket.destroy();
188689
- resolve9(true);
188773
+ resolve10(true);
188690
188774
  });
188691
188775
  socket.on("timeout", () => {
188692
188776
  socket.destroy();
188693
- resolve9(false);
188777
+ resolve10(false);
188694
188778
  });
188695
188779
  socket.on("error", () => {
188696
188780
  socket.destroy();
188697
- resolve9(false);
188781
+ resolve10(false);
188698
188782
  });
188699
188783
  socket.connect(port, host);
188700
188784
  });
188701
188785
  }
188702
188786
  async function stopProcess3(proc) {
188703
- return new Promise((resolve9) => {
188787
+ return new Promise((resolve10) => {
188704
188788
  if (proc.killed || proc.exitCode !== null) {
188705
- resolve9();
188789
+ resolve10();
188706
188790
  return;
188707
188791
  }
188708
188792
  proc.once("exit", () => {
188709
188793
  clearTimeout(forceKillTimeout);
188710
- resolve9();
188794
+ resolve10();
188711
188795
  });
188712
188796
  proc.kill("SIGTERM");
188713
188797
  const forceKillTimeout = setTimeout(() => {
@@ -188718,7 +188802,7 @@ async function stopProcess3(proc) {
188718
188802
  });
188719
188803
  }
188720
188804
  function sleep4(ms) {
188721
- return new Promise((resolve9) => setTimeout(resolve9, ms));
188805
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
188722
188806
  }
188723
188807
 
188724
188808
  // src/lib/dev/sync-detector.ts
@@ -188727,10 +188811,14 @@ function detectSyncDatabases(config) {
188727
188811
  for (const service of config.services) {
188728
188812
  if (!service.env) continue;
188729
188813
  for (const value of Object.values(service.env)) {
188730
- if (typeof value === "object" && value.type === "postgres") {
188731
- const attr = value.attribute;
188732
- if (attr === "sync.url" || attr === "sync.secret") {
188733
- needsSync.add(value.name);
188814
+ if (typeof value !== "object") continue;
188815
+ const refs = value.type === "interpolated" ? value.parts.filter((p) => p.type === "ref").map((p) => p.ref) : [value];
188816
+ for (const ref of refs) {
188817
+ if (ref.type === "postgres") {
188818
+ const attr = ref.attribute;
188819
+ if (attr === "sync.url" || attr === "sync.secret") {
188820
+ needsSync.add(ref.name);
188821
+ }
188734
188822
  }
188735
188823
  }
188736
188824
  }
@@ -189025,8 +189113,9 @@ function findRequiredResources(service) {
189025
189113
  const required = { postgres: [], redis: [], storage: [] };
189026
189114
  if (service.env) {
189027
189115
  for (const value of Object.values(service.env)) {
189028
- if (typeof value === "object" && value !== null) {
189029
- const ref = value;
189116
+ if (typeof value !== "object" || value === null) continue;
189117
+ const refs = value.type === "interpolated" ? value.parts.filter((p) => p.type === "ref").map((p) => p.ref) : [value];
189118
+ for (const ref of refs) {
189030
189119
  if (ref.type === "postgres" && !required.postgres.includes(ref.name)) {
189031
189120
  required.postgres.push(ref.name);
189032
189121
  } else if (ref.type === "redis" && !required.redis.includes(ref.name)) {
@@ -189335,7 +189424,7 @@ var ProxyRegistryManager = class {
189335
189424
  * This catches cases where the owner process is alive but the proxy has crashed.
189336
189425
  */
189337
189426
  isProxyListening(port, timeoutMs = 1e3) {
189338
- return new Promise((resolve9) => {
189427
+ return new Promise((resolve10) => {
189339
189428
  const socket = new net4.Socket();
189340
189429
  let resolved = false;
189341
189430
  const cleanup = () => {
@@ -189347,15 +189436,15 @@ var ProxyRegistryManager = class {
189347
189436
  socket.setTimeout(timeoutMs);
189348
189437
  socket.on("connect", () => {
189349
189438
  cleanup();
189350
- resolve9(true);
189439
+ resolve10(true);
189351
189440
  });
189352
189441
  socket.on("timeout", () => {
189353
189442
  cleanup();
189354
- resolve9(false);
189443
+ resolve10(false);
189355
189444
  });
189356
189445
  socket.on("error", () => {
189357
189446
  cleanup();
189358
- resolve9(false);
189447
+ resolve10(false);
189359
189448
  });
189360
189449
  socket.connect(port, "127.0.0.1");
189361
189450
  });
@@ -189406,7 +189495,7 @@ var ProxyRegistryManager = class {
189406
189495
  }
189407
189496
  continue;
189408
189497
  }
189409
- await new Promise((resolve9) => setTimeout(resolve9, 100));
189498
+ await new Promise((resolve10) => setTimeout(resolve10, 100));
189410
189499
  } else {
189411
189500
  throw e;
189412
189501
  }
@@ -189890,7 +189979,7 @@ function DevUI({ instanceKey, tunnelEnabled }) {
189890
189979
  ...tunnelsRef.current.map((tunnel) => tunnel.stop())
189891
189980
  ]);
189892
189981
  if (tunnelsRef.current.length > 0) {
189893
- await new Promise((resolve9) => setTimeout(resolve9, 1500));
189982
+ await new Promise((resolve10) => setTimeout(resolve10, 1500));
189894
189983
  }
189895
189984
  electricInstancesRef.current = [];
189896
189985
  reshapeWatchersRef.current = [];
@@ -190400,6 +190489,14 @@ Add them to the config block in specific.local`);
190400
190489
  }
190401
190490
  }
190402
190491
  const services2 = [];
190492
+ function resolveServiceCwd(service) {
190493
+ if (service.root) return path18.resolve(process.cwd(), service.root);
190494
+ if (service.build) {
190495
+ const build = config2.builds.find((b) => b.name === service.build.name);
190496
+ if (build?.root) return path18.resolve(process.cwd(), build.root);
190497
+ }
190498
+ return process.cwd();
190499
+ }
190403
190500
  for (const service of config2.services) {
190404
190501
  if (cancelled) break;
190405
190502
  if (service.serve) {
@@ -190423,7 +190520,8 @@ Add them to the config block in specific.local`);
190423
190520
  endpointPorts,
190424
190521
  serviceEndpoints,
190425
190522
  (line) => addLog(line, colorMap),
190426
- publicUrls
190523
+ publicUrls,
190524
+ resolveServiceCwd(service)
190427
190525
  );
190428
190526
  services2.push(running);
190429
190527
  startedServices.push(running);
@@ -190468,7 +190566,9 @@ Add them to the config block in specific.local`);
190468
190566
  configs,
190469
190567
  endpointPorts,
190470
190568
  serviceEndpoints,
190471
- (line) => addLog(line, colorMap)
190569
+ (line) => addLog(line, colorMap),
190570
+ void 0,
190571
+ resolveServiceCwd(service)
190472
190572
  );
190473
190573
  newServices.push(running);
190474
190574
  } catch (err) {
@@ -190978,7 +191078,7 @@ import * as path21 from "path";
190978
191078
  // src/lib/deploy/build-tester.ts
190979
191079
  import { spawn as spawn5 } from "child_process";
190980
191080
  import { existsSync as existsSync20 } from "fs";
190981
- import { join as join21 } from "path";
191081
+ import { join as join21, resolve as resolve7 } from "path";
190982
191082
  function getDependencyInstallCommand(build, projectDir) {
190983
191083
  switch (build.base) {
190984
191084
  case "node":
@@ -190988,9 +191088,10 @@ function getDependencyInstallCommand(build, projectDir) {
190988
191088
  return "yarn install --frozen-lockfile";
190989
191089
  } else if (existsSync20(join21(projectDir, "package-lock.json"))) {
190990
191090
  return "npm ci";
190991
- } else {
191091
+ } else if (existsSync20(join21(projectDir, "package.json"))) {
190992
191092
  return "npm install";
190993
191093
  }
191094
+ return null;
190994
191095
  case "python":
190995
191096
  if (existsSync20(join21(projectDir, "poetry.lock"))) {
190996
191097
  return "poetry install --no-interaction";
@@ -191014,7 +191115,7 @@ function getDependencyInstallCommand(build, projectDir) {
191014
191115
  }
191015
191116
  }
191016
191117
  function runCommand2(command, projectDir, buildName) {
191017
- return new Promise((resolve9) => {
191118
+ return new Promise((resolve10) => {
191018
191119
  const stdout = [];
191019
191120
  const stderr = [];
191020
191121
  writeLog("build-test", `[${buildName}] Running: ${command}`);
@@ -191044,7 +191145,7 @@ function runCommand2(command, projectDir, buildName) {
191044
191145
  });
191045
191146
  child.on("error", (err) => {
191046
191147
  writeLog("build-test:error", `[${buildName}] Failed to start: ${err.message}`);
191047
- resolve9({
191148
+ resolve10({
191048
191149
  success: false,
191049
191150
  output: `Failed to start command: ${err.message}`
191050
191151
  });
@@ -191053,10 +191154,10 @@ function runCommand2(command, projectDir, buildName) {
191053
191154
  const output = [...stdout, ...stderr].join("");
191054
191155
  if (code === 0) {
191055
191156
  writeLog("build-test", `[${buildName}] Command succeeded (exit code 0)`);
191056
- resolve9({ success: true, output });
191157
+ resolve10({ success: true, output });
191057
191158
  } else {
191058
191159
  writeLog("build-test:error", `[${buildName}] Command failed with exit code ${code}`);
191059
- resolve9({
191160
+ resolve10({
191060
191161
  success: false,
191061
191162
  output: output || `Exit code: ${code}`
191062
191163
  });
@@ -191067,7 +191168,7 @@ function runCommand2(command, projectDir, buildName) {
191067
191168
  async function testBuild(build, projectDir) {
191068
191169
  const startTime = Date.now();
191069
191170
  const outputs = [];
191070
- const workDir = projectDir;
191171
+ const workDir = build.root ? resolve7(projectDir, build.root) : projectDir;
191071
191172
  writeLog("build-test", `Starting test for build "${build.name}" (base: ${build.base}, workDir: ${workDir})`);
191072
191173
  const depsCommand = getDependencyInstallCommand(build, workDir);
191073
191174
  if (depsCommand) {
@@ -192328,8 +192429,17 @@ async function execCommand(serviceName, command, instanceKey = "default") {
192328
192429
  };
192329
192430
  process.on("SIGINT", () => handleSignal("SIGINT"));
192330
192431
  process.on("SIGTERM", () => handleSignal("SIGTERM"));
192432
+ let effectiveCwd = process.cwd();
192433
+ if (service.root) {
192434
+ effectiveCwd = path22.resolve(process.cwd(), service.root);
192435
+ } else if (service.build) {
192436
+ const build = config.builds.find((b) => b.name === service.build.name);
192437
+ if (build?.root) {
192438
+ effectiveCwd = path22.resolve(process.cwd(), build.root);
192439
+ }
192440
+ }
192331
192441
  child = spawn6(command[0], command.slice(1), {
192332
- cwd: process.cwd(),
192442
+ cwd: effectiveCwd,
192333
192443
  env: {
192334
192444
  ...process.env,
192335
192445
  ...resolvedEnv
@@ -192744,20 +192854,22 @@ function CleanUI({ instanceKey }) {
192744
192854
  }
192745
192855
  } else {
192746
192856
  const keysDir = path25.join(specificDir, "keys");
192747
- if (fs27.existsSync(keysDir)) {
192748
- const keys = fs27.readdirSync(keysDir).filter(
192749
- (f) => fs27.statSync(path25.join(keysDir, f)).isDirectory()
192750
- );
192751
- for (const key of keys) {
192752
- const stateManager2 = new InstanceStateManager(projectRoot, key);
192753
- const existingInstances2 = await stateManager2.getExistingInstances();
192754
- if (existingInstances2) {
192755
- setState({
192756
- status: "error",
192757
- error: `Cannot clean while 'specific dev' is running for key "${key}" (PID ${existingInstances2.owner.pid}). Please stop it first.`
192758
- });
192759
- return;
192760
- }
192857
+ if (!fs27.existsSync(keysDir)) {
192858
+ setState({ status: "nothing" });
192859
+ return;
192860
+ }
192861
+ const keys = fs27.readdirSync(keysDir).filter(
192862
+ (f) => fs27.statSync(path25.join(keysDir, f)).isDirectory()
192863
+ );
192864
+ for (const key of keys) {
192865
+ const stateManager2 = new InstanceStateManager(projectRoot, key);
192866
+ const existingInstances2 = await stateManager2.getExistingInstances();
192867
+ if (existingInstances2) {
192868
+ setState({
192869
+ status: "error",
192870
+ error: `Cannot clean while 'specific dev' is running for key "${key}" (PID ${existingInstances2.owner.pid}). Please stop it first.`
192871
+ });
192872
+ return;
192761
192873
  }
192762
192874
  }
192763
192875
  const stateManager = new InstanceStateManager(projectRoot);
@@ -192771,7 +192883,7 @@ function CleanUI({ instanceKey }) {
192771
192883
  }
192772
192884
  setState({ status: "cleaning" });
192773
192885
  try {
192774
- fs27.rmSync(specificDir, { recursive: true, force: true });
192886
+ fs27.rmSync(keysDir, { recursive: true, force: true });
192775
192887
  setState({ status: "success" });
192776
192888
  } catch (err) {
192777
192889
  setState({
@@ -192787,16 +192899,16 @@ function CleanUI({ instanceKey }) {
192787
192899
  return /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text8, { color: "blue" }, /* @__PURE__ */ React8.createElement(Spinner6, { type: "dots" })), /* @__PURE__ */ React8.createElement(Text8, null, " Checking for running instances..."));
192788
192900
  }
192789
192901
  if (state.status === "cleaning") {
192790
- return /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text8, { color: "blue" }, /* @__PURE__ */ React8.createElement(Spinner6, { type: "dots" })), /* @__PURE__ */ React8.createElement(Text8, null, " Removing .specific directory..."));
192902
+ return /* @__PURE__ */ React8.createElement(Box8, null, /* @__PURE__ */ React8.createElement(Text8, { color: "blue" }, /* @__PURE__ */ React8.createElement(Spinner6, { type: "dots" })), /* @__PURE__ */ React8.createElement(Text8, null, " Cleaning app data..."));
192791
192903
  }
192792
192904
  if (state.status === "error") {
192793
192905
  return /* @__PURE__ */ React8.createElement(Text8, { color: "red" }, "Error: ", state.error);
192794
192906
  }
192795
192907
  if (state.status === "nothing") {
192796
- const target2 = instanceKey ? `key "${instanceKey}"` : ".specific directory";
192908
+ const target2 = instanceKey ? `key "${instanceKey}"` : "app data";
192797
192909
  return /* @__PURE__ */ React8.createElement(Text8, { color: "yellow" }, "Nothing to clean (", target2, " does not exist)");
192798
192910
  }
192799
- const target = instanceKey ? `key "${instanceKey}"` : ".specific directory";
192911
+ const target = instanceKey ? `key "${instanceKey}"` : "app data";
192800
192912
  return /* @__PURE__ */ React8.createElement(Text8, { color: "green" }, "Cleaned ", target);
192801
192913
  }
192802
192914
  function cleanCommand(instanceKey) {
@@ -192934,7 +193046,7 @@ function betaCommand() {
192934
193046
  var program = new Command();
192935
193047
  var env = "production";
192936
193048
  var envLabel = env !== "production" ? `[${env.toUpperCase()}] ` : "";
192937
- program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.50").enablePositionalOptions();
193049
+ program.name("specific").description(`${envLabel}Infrastructure-as-code for coding agents`).version("0.1.52").enablePositionalOptions();
192938
193050
  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));
192939
193051
  program.command("docs [topic]").description("Fetch LLM-optimized documentation").action(docsCommand);
192940
193052
  program.command("check").description("Validate specific.hcl configuration").action(checkCommand);